From 1545343e140fc1a1279e641bcfd87473ff410f8e Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Tue, 15 Oct 2024 10:46:10 +0200 Subject: [PATCH] Update data structure to support new obfuscation selection --- .../WireGuardObfuscationSettings.swift | 155 ++++++++++++++---- ios/MullvadVPN/UI appearance/UIMetrics.swift | 1 + .../Account/AccountViewController.swift | 4 +- .../VPNSettings/VPNSettingsCellFactory.swift | 14 +- .../VPNSettings/VPNSettingsDataSource.swift | 29 ++-- .../VPNSettings/VPNSettingsViewModel.swift | 14 +- .../Actor/ProtocolObfuscator.swift | 8 +- 7 files changed, 170 insertions(+), 55 deletions(-) diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift index e55ac8538af7..1f43524c3aaa 100644 --- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift +++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift @@ -8,54 +8,153 @@ import Foundation -/// Whether UDP-over-TCP obfuscation is enabled +/// Whether obfuscation is enabled and which method is used /// /// `.automatic` means an algorithm will decide whether to use it or not. public enum WireGuardObfuscationState: Codable { - case automatic + @available(*, deprecated, renamed: "udpTcp") case on + + case automatic + case udpOverTcp + case shadowsocks case off + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + var allKeys = ArraySlice(container.allKeys) + guard let key = allKeys.popFirst(), allKeys.isEmpty else { + throw DecodingError.typeMismatch( + WireGuardObfuscationState.self, + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one.", + underlyingError: nil + ) + ) + } + + switch key { + case .automatic: + self = .automatic + case .on, .udpOverTcp: + self = .udpOverTcp + case .shadowsocks: + self = .shadowsocks + case .off: + self = .off + } + } } -/// The port to select when using UDP-over-TCP obfuscation -/// -/// `.automatic` means an algorith will decide between using `port80` or `port5001` -public enum WireGuardObfuscationPort: UInt16, Codable, CaseIterable { - case automatic = 0 - case port80 = 80 - case port5001 = 5001 +public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomStringConvertible { + case automatic + case port80 + case port5001 - /// The `UInt16` representation of the port. - /// - Returns: `0` if `.automatic`, `80` or `5001` otherwise. - public var portValue: UInt16 { - self == .automatic ? 0 : rawValue + public var portValue: UInt16? { + switch self { + case .automatic: + nil + case .port80: + 80 + case .port5001: + 5001 + } } - public init?(rawValue: UInt16) { - switch rawValue { - case 80: - self = .port80 - case 5001: - self = .port5001 - default: self = .automatic + public var description: String { + switch self { + case .automatic: + NSLocalizedString( + "WIREGUARD_OBFUSCATION_UDP_TCP_PORT_AUTOMATIC", + tableName: "VPNSettings", + value: "Automatic", + comment: "" + ) + case .port80: + "80" + case .port5001: + "5001" } } +} - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let decodedValue = try? container.decode(UInt16.self) +public enum WireGuardObfuscationShadowsockPort: Codable, Equatable, CustomStringConvertible { + case automatic + case custom(UInt16) - let port = WireGuardObfuscationPort.allCases.first(where: { $0.rawValue == decodedValue }) - self = port ?? .automatic + public var portValue: UInt16? { + switch self { + case .automatic: + nil + case .custom(let port): + port + } + } + + public var description: String { + switch self { + case .automatic: + NSLocalizedString( + "WIREGUARD_OBFUSCATION_SHADOWSOCKS_PORT_AUTOMATIC", + tableName: "VPNSettings", + value: "Automatic", + comment: "" + ) + case .custom(let port): + String(port) + } } } +enum WireGuardObfuscationPort: UInt16, Codable { + case automatic = 0 + case port80 = 80 + case port5001 = 5001 +} + public struct WireGuardObfuscationSettings: Codable, Equatable { + @available(*, deprecated, message: "Use `udpOverTcpPort` instead") + private var port: WireGuardObfuscationPort = .automatic + public let state: WireGuardObfuscationState - public let port: WireGuardObfuscationPort + public let udpOverTcpPort: WireGuardObfuscationUdpOverTcpPort + public let shadowsocksPort: WireGuardObfuscationShadowsockPort - public init(state: WireGuardObfuscationState = .automatic, port: WireGuardObfuscationPort = .automatic) { + public init( + state: WireGuardObfuscationState = .automatic, + udpOverTcpPort: WireGuardObfuscationUdpOverTcpPort = .automatic, + shadowsocksPort: WireGuardObfuscationShadowsockPort = .automatic + ) { self.state = state - self.port = port + self.udpOverTcpPort = udpOverTcpPort + self.shadowsocksPort = shadowsocksPort + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + state = try container.decode(WireGuardObfuscationState.self, forKey: .state) + shadowsocksPort = try container.decodeIfPresent( + WireGuardObfuscationShadowsockPort.self, + forKey: .shadowsocksPort + ) ?? .automatic + + if let port = try? container.decodeIfPresent(WireGuardObfuscationUdpOverTcpPort.self, forKey: .udpOverTcpPort) { + udpOverTcpPort = port + } else if let port = try? container.decodeIfPresent(WireGuardObfuscationPort.self, forKey: .port) { + switch port { + case .automatic: + udpOverTcpPort = .automatic + case .port80: + udpOverTcpPort = .port80 + case .port5001: + udpOverTcpPort = .port5001 + } + } else { + udpOverTcpPort = .automatic + } } } diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index ce108dceb84d..7401c179798c 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -76,6 +76,7 @@ enum UIMetrics { static let layoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 12) static let inputCellTextFieldLayoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) static let selectableSettingsCellLeftViewSpacing: CGFloat = 12 + static let settingsCellRightViewSpacing: CGFloat = 12 static let checkableSettingsCellLeftViewSpacing: CGFloat = 20 /// Cell layout margins used in table views that use inset style. diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index f39a228e6681..3921fab2d3ce 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -198,8 +198,8 @@ class AccountViewController: UIViewController { purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled contentView.accountDeviceRow.setButtons(enabled: isInteractionEnabled) contentView.accountTokenRowView.setButtons(enabled: isInteractionEnabled) - contentView.restorePurchasesView.setButtons(enabled: isInteractionEnabled) - contentView.logoutButton.isEnabled = isInteractionEnabled + contentView.restorePurchasesView.setButtons(enabled: false) + contentView.logoutButton.isEnabled = false contentView.redeemVoucherButton.isEnabled = isInteractionEnabled contentView.deleteButton.isEnabled = isInteractionEnabled navigationItem.rightBarButtonItem?.isEnabled = isInteractionEnabled diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index 51038d4b68c0..526fb1c16c8c 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -145,14 +145,16 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { value: "UDP-over-TCP", comment: "" ) + #if DEBUG cell.detailTitleLabel.text = String(format: NSLocalizedString( "WIREGUARD_OBFUSCATION_UDP_TCP_PORT", tableName: "VPNSettings", - value: "Port: %d", + value: "Port: %@", comment: "" - ), viewModel.obfuscationPort.portValue) + ), viewModel.obfuscationUpdOverTcpPort.description) #endif + cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() @@ -169,14 +171,16 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { value: "Shadowsocks", comment: "" ) + #if DEBUG cell.detailTitleLabel.text = String(format: NSLocalizedString( "WIREGUARD_OBFUSCATION_SHADOWSOCKS_PORT", tableName: "VPNSettings", - value: "Port: %d", + value: "Port: %@", comment: "" - ), viewModel.obfuscationPort.portValue) + ), viewModel.obfuscationShadowsocksPort.description) #endif + cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() @@ -199,7 +203,7 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { case let .wireGuardObfuscationPort(port): guard let cell = cell as? SelectableSettingsCell else { return } - let portString = port == 0 ? "Automatic" : "\(port)" + let portString = port.description cell.titleLabel.text = NSLocalizedString( "WIREGUARD_OBFUSCATION_PORT_LABEL", tableName: "VPNSettings", diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 5c0f8856d3ac..7b958fff2ae5 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -79,7 +79,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscationUdpOverTcp case wireGuardObfuscationShadowsocks case wireGuardObfuscationOff - case wireGuardObfuscationPort(_ port: UInt16) + case wireGuardObfuscationPort(_ port: WireGuardObfuscationUdpOverTcpPort) case quantumResistanceAutomatic case quantumResistanceOn case quantumResistanceOff @@ -107,7 +107,11 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< } static var wireGuardObfuscationPort: [Item] { - [.wireGuardObfuscationPort(0), .wireGuardObfuscationPort(80), .wireGuardObfuscationPort(5001)] + [ + .wireGuardObfuscationPort(.automatic), + .wireGuardObfuscationPort(.port80), + .wireGuardObfuscationPort(.port5001) + ] } static var quantumResistance: [Item] { @@ -178,7 +182,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< private var obfuscationSettings: WireGuardObfuscationSettings { WireGuardObfuscationSettings( state: viewModel.obfuscationState, - port: viewModel.obfuscationPort + udpOverTcpPort: viewModel.obfuscationUpdOverTcpPort, + shadowsocksPort: viewModel.obfuscationShadowsocksPort ) } @@ -192,7 +197,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let obfuscationStateItem: Item = switch viewModel.obfuscationState { case .automatic: .wireGuardObfuscationAutomatic case .off: .wireGuardObfuscationOff - case .on: .wireGuardObfuscationUdpOverTcp + case .on, .udpOverTcp: .wireGuardObfuscationUdpOverTcp + case .shadowsocks: .wireGuardObfuscationShadowsocks } let quantumResistanceItem: Item = switch viewModel.quantumResistance { @@ -201,7 +207,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .on: .quantumResistanceOn } - let obfuscationPortItem: Item = .wireGuardObfuscationPort(viewModel.obfuscationPort.portValue) + let obfuscationPortItem: Item = .wireGuardObfuscationPort(viewModel.obfuscationUpdOverTcpPort) return [ wireGuardPortItem, @@ -308,13 +314,13 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< selectObfuscationState(.automatic) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) case .wireGuardObfuscationUdpOverTcp: - selectObfuscationState(.on) + selectObfuscationState(.udpOverTcp) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) - // TODO: When ready, add implementation for selected obfuscation. + // TODO: When ready, add implementation for selected obfuscation (navigate to new view etc). case .wireGuardObfuscationShadowsocks: - selectObfuscationState(.on) + selectObfuscationState(.shadowsocks) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) - // TODO: When ready, add implementation for selected obfuscation. + // TODO: When ready, add implementation for selected obfuscation (navigate to new view etc). case .wireGuardObfuscationOff: selectObfuscationState(.off) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) @@ -656,9 +662,8 @@ extension VPNSettingsDataSource: VPNSettingsCellEventHandler { viewModel.setWireGuardObfuscationState(state) } - func selectObfuscationPort(_ port: UInt16) { - let selectedPort = WireGuardObfuscationPort(rawValue: port)! - viewModel.setWireGuardObfuscationPort(selectedPort) + func selectObfuscationPort(_ port: WireGuardObfuscationUdpOverTcpPort) { + viewModel.setWireGuardObfuscationUdpOverTcpPort(port) } func selectQuantumResistance(_ state: TunnelQuantumResistance) { diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift index 457a8928cae0..90e31ede7347 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift @@ -97,7 +97,8 @@ struct VPNSettingsViewModel: Equatable { var availableWireGuardPortRanges: [[UInt16]] = [] private(set) var obfuscationState: WireGuardObfuscationState - private(set) var obfuscationPort: WireGuardObfuscationPort + private(set) var obfuscationUpdOverTcpPort: WireGuardObfuscationUdpOverTcpPort + private(set) var obfuscationShadowsocksPort: WireGuardObfuscationShadowsockPort private(set) var quantumResistance: TunnelQuantumResistance private(set) var multihopState: MultihopState @@ -178,8 +179,12 @@ struct VPNSettingsViewModel: Equatable { obfuscationState = newState } - mutating func setWireGuardObfuscationPort(_ newPort: WireGuardObfuscationPort) { - obfuscationPort = newPort + mutating func setWireGuardObfuscationShadowsockPort(_ newPort: WireGuardObfuscationShadowsockPort) { + obfuscationShadowsocksPort = newPort + } + + mutating func setWireGuardObfuscationUdpOverTcpPort(_ newPort: WireGuardObfuscationUdpOverTcpPort) { + obfuscationUpdOverTcpPort = newPort } mutating func setQuantumResistance(_ newState: TunnelQuantumResistance) { @@ -242,7 +247,8 @@ struct VPNSettingsViewModel: Equatable { wireGuardPort = tunnelSettings.relayConstraints.port.value obfuscationState = tunnelSettings.wireGuardObfuscation.state - obfuscationPort = tunnelSettings.wireGuardObfuscation.port + obfuscationUpdOverTcpPort = tunnelSettings.wireGuardObfuscation.udpOverTcpPort + obfuscationShadowsocksPort = tunnelSettings.wireGuardObfuscation.shadowsocksPort quantumResistance = tunnelSettings.tunnelQuantumResistance multihopState = tunnelSettings.tunnelMultihopState diff --git a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift index e5a0c694b660..d43fafa6f5ef 100644 --- a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift +++ b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift @@ -42,7 +42,7 @@ public class ProtocolObfuscator: ProtocolObfuscat let shouldObfuscate = switch settings.obfuscation.state { case .automatic: retryAttempts % 4 == 2 || retryAttempts % 4 == 3 - case .on: + case .on, .udpOverTcp, .shadowsocks: true case .off: false @@ -52,15 +52,15 @@ public class ProtocolObfuscator: ProtocolObfuscat tunnelObfuscator = nil return endpoint } - var tcpPort = settings.obfuscation.port + var tcpPort = settings.obfuscation.udpOverTcpPort if tcpPort == .automatic { tcpPort = retryAttempts % 2 == 0 ? .port80 : .port5001 } let obfuscator = Obfuscator( remoteAddress: obfuscatedEndpoint.ipv4Relay.ip, - tcpPort: tcpPort.portValue + tcpPort: tcpPort.portValue ?? 0 ) - remotePort = tcpPort.portValue + remotePort = tcpPort.portValue ?? 0 obfuscator.start() tunnelObfuscator = obfuscator