diff options
13 files changed, 192 insertions, 60 deletions
diff --git a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift index dcc7cf05da..f73509affe 100644 --- a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift +++ b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift @@ -17,9 +17,9 @@ public struct ObfuscationMethodSelector { ) -> WireGuardObfuscationState { // TODO: Revisit this when QUIC obfuscation is added if tunnelSettings.wireGuardObfuscation.state == .automatic { - if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 4) { + if connectionAttemptCount.isOrdered(nth: 2, forEverySetOf: 3) { .shadowsocks - } else if connectionAttemptCount.isOrdered(nth: 4, forEverySetOf: 4) { + } else if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 3) { .udpOverTcp } else { .off diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift index 14742aef09..8393f997e6 100644 --- a/ios/MullvadREST/Relay/RelaySelector.swift +++ b/ios/MullvadREST/Relay/RelaySelector.swift @@ -9,8 +9,6 @@ import MullvadSettings import MullvadTypes -private let defaultPort: UInt16 = 443 - public enum RelaySelector { // MARK: - public @@ -83,17 +81,11 @@ public enum RelaySelector { rawPortRanges: [[UInt16]], numberOfFailedAttempts: UInt ) -> UInt16? { - switch portConstraint { + return switch portConstraint { case let .only(port): - return port - + port case .any: - // 1. First attempt should pick a random port. - // 2. The second should pick port 443. - // 3. Repeat steps 1 and 2. - let useDefaultPort = numberOfFailedAttempts.isOrdered(nth: 2, forEverySetOf: 2) - - return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges) + pickRandomPort(rawPortRanges: rawPortRanges) } } diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift index c52a0a2faf..1a6487dca9 100644 --- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -20,7 +20,14 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable { tunnelSettings: LatestTunnelSettings, connectionAttemptCount: UInt ) throws -> SelectedRelays { - let obfuscation = try prepareObfuscation(for: tunnelSettings, connectionAttemptCount: connectionAttemptCount) + let relays = try relayCache.read().relays + try validateWireguardPort(tunnelSettings, relays: relays) + + let obfuscation = try prepareObfuscation( + for: tunnelSettings, + connectionAttemptCount: connectionAttemptCount, + relays: relays + ) return switch tunnelSettings.tunnelMultihopState { case .off: @@ -41,7 +48,12 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable { } public func findCandidates(tunnelSettings: LatestTunnelSettings) throws -> RelayCandidates { - let obfuscation = try prepareObfuscation(for: tunnelSettings, connectionAttemptCount: 0) + let relays = try relayCache.read().relays + let obfuscation = try prepareObfuscation( + for: tunnelSettings, + connectionAttemptCount: 0, + relays: relays + ) let findCandidates: (REST.ServerRelaysResponse, Bool) throws -> [RelayWithLocation<REST.ServerRelay>] = { relays, daitaEnabled in @@ -68,12 +80,36 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable { private func prepareObfuscation( for tunnelSettings: LatestTunnelSettings, - connectionAttemptCount: UInt + connectionAttemptCount: UInt, + relays: REST.ServerRelaysResponse ) throws -> ObfuscatorPortSelection { - let relays = try relayCache.read().relays return try ObfuscatorPortSelector(relays: relays).obfuscate( tunnelSettings: tunnelSettings, connectionAttemptCount: connectionAttemptCount ) } + + private func validateWireguardPort( + _ tunnelSettings: LatestTunnelSettings, + relays: REST.ServerRelaysResponse + ) throws { + switch tunnelSettings.wireGuardObfuscation.state { + case .automatic, .off: + if case let .only(port) = tunnelSettings.relayConstraints.port { + let isPortWithinValidWireGuardRanges: Bool = + relays.wireguard.portRanges + .contains { range in + if let minPort = range.first, let maxPort = range.last { + return (minPort ... maxPort).contains(port) + } + return false + } + guard isPortWithinValidWireGuardRanges else { + throw NoRelaysSatisfyingConstraintsError(.invalidPort) + } + } + case .on, .udpOverTcp, .quic, .shadowsocks: + break + } + } } diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index b68577ba14..7aa56ed588 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -823,6 +823,12 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo updateDeviceInfo(deviceState: tunnelManager.deviceState) case .latestChangesInAppNotificationProvider: router.present(.changelog) + case .tunnelStatusNotificationProvider: + switch response.actionIdentifier { + case TunnelStatusNotificationProvider.ActionIdentifier.showVPNSettings.rawValue: + router.present(.settings(.vpnSettings)) + default: break + } default: return } } diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index e0135cb44f..43eba09131 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -8,8 +8,13 @@ import Foundation import PacketTunnelCore +import UIKit final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider, @unchecked Sendable { + enum ActionIdentifier: String { + case showVPNSettings + } + private var isWaitingForConnectivity = false private var noNetwork = false private var packetTunnelError: BlockedStateReason? @@ -135,7 +140,19 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific } private func notificationDescription(for packetTunnelError: BlockedStateReason) -> InAppNotificationDescriptor { - InAppNotificationDescriptor( + let tapAction: InAppNotificationAction? = switch packetTunnelError { + case .noRelaysSatisfyingPortConstraints: + InAppNotificationAction { + NotificationManager.shared + .notificationProvider( + self, + didReceiveAction: "\(ActionIdentifier.showVPNSettings)" + ) + } + default: + nil + } + return InAppNotificationDescriptor( identifier: identifier, style: .error, title: NSLocalizedString( @@ -143,13 +160,23 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific value: "BLOCKING INTERNET", comment: "" ), - body: .init(string: String( - format: NSLocalizedString( - "TUNNEL_BLOCKED_INAPP_NOTIFICATION_BODY", - value: localizedReasonForBlockedStateError(packetTunnelError), - comment: "" - ) - )) + body: createNotificationBody(localizedReasonForBlockedStateError(packetTunnelError)), + tapAction: tapAction + ) + } + + private func createNotificationBody(_ string: String) -> NSAttributedString { + NSAttributedString( + markdownString: NSLocalizedString( + "LATEST_CHANGES_IN_APP_NOTIFICATION_BODY", + value: string, + comment: "" + ), + options: MarkdownStylingOptions(font: UIFont.preferredFont(forTextStyle: .body)), + applyEffect: { markdownType, _ in + guard case .bold = markdownType else { return [:] } + return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor] + } ) } @@ -248,6 +275,8 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific errorString = "No DAITA compatible servers match your location settings. Try changing location." case .noRelaysSatisfyingConstraints: errorString = "No servers match your settings, try changing server or other settings." + case .noRelaysSatisfyingPortConstraints: + errorString = "The selected WireGuard port is not supported, please change it under **VPN settings**." case .invalidAccount: errorString = "You are logged in with an invalid account number. Please log out and try another one." case .deviceLoggedOut: diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift index 6fb2be5a25..3694076db0 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift @@ -29,7 +29,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { connectionAttemptCount: attempt, tunnelSettings: tunnelSettings ) - if attempt.isOrdered(nth: 1, forEverySetOf: 4) || attempt.isOrdered(nth: 2, forEverySetOf: 4) { + if attempt.isOrdered(nth: 1, forEverySetOf: 3) { XCTAssertEqual(method, .off) } else { XCTAssertNotEqual(method, .off) @@ -53,7 +53,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { connectionAttemptCount: attempt, tunnelSettings: tunnelSettings ) - if attempt.isOrdered(nth: 3, forEverySetOf: 4) { + if attempt.isOrdered(nth: 2, forEverySetOf: 3) { XCTAssertEqual(method, .shadowsocks) } else { XCTAssertNotEqual(method, .shadowsocks) @@ -77,7 +77,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { connectionAttemptCount: attempt, tunnelSettings: tunnelSettings ) - if attempt.isOrdered(nth: 4, forEverySetOf: 4) { + if attempt.isOrdered(nth: 3, forEverySetOf: 3) { XCTAssertEqual(method, .udpOverTcp) } else { XCTAssertNotEqual(method, .udpOverTcp) diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift index e383aa1b25..fcc71b4653 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift @@ -15,7 +15,6 @@ import Network import XCTest private let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]] -private let defaultPort: UInt16 = 443 class RelaySelectorTests: XCTestCase { let sampleRelays = ServerRelaysResponseStubs.sampleRelays @@ -114,25 +113,13 @@ class RelaySelectorTests: XCTestCase { XCTAssertEqual(result.endpoint.ipv4Relay.port, 1) } - func testRandomPortSelectionWithFailedAttempts() throws { + func testRandomPortSelection() throws { let constraints = RelayConstraints( exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) ) let allPorts = portRanges.flatMap { $0 } - var result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0) - XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port)) - - result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 1) - XCTAssertEqual(result.endpoint.ipv4Relay.port, defaultPort) - - result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 2) - XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port)) - - result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 3) - XCTAssertEqual(result.endpoint.ipv4Relay.port, defaultPort) - - result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 4) + let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0) XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port)) } diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift index b84cb528b8..0302ad5380 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift @@ -120,4 +120,80 @@ class RelaySelectorWrapperTests: XCTestCase { let selectedRelays = try wrapper.selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) XCTAssertNotNil(selectedRelays.entry) } + + func testValidWireguardPortDoesNotThrow() throws { + let wrapper = RelaySelectorWrapper(relayCache: relayCache) + + let settings = LatestTunnelSettings( + relayConstraints: .init( + port: + .only( + ServerRelaysResponseStubs.sampleRelays.wireguard.portRanges.first!.first! + ) + ) + ) + + XCTAssertNoThrow( + try wrapper + .selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) + ) + } + + func testInvalidWireguardPortThrows() throws { + let wrapper = RelaySelectorWrapper(relayCache: relayCache) + + var settings = LatestTunnelSettings( + relayConstraints: .init(port: .only(1)), + wireGuardObfuscation: .init(state: .automatic) + ) + + XCTAssertThrowsError( + try wrapper + .selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) + ) + + settings = LatestTunnelSettings( + relayConstraints: .init(port: .only(1)), + wireGuardObfuscation: .init(state: .off) + ) + + XCTAssertThrowsError( + try wrapper + .selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) + ) + } + + func testInvalidWireguardPortDoesNotThrowWhenObfuscated() throws { + let wrapper = RelaySelectorWrapper(relayCache: relayCache) + + var settings = LatestTunnelSettings( + relayConstraints: .init(port: .only(1)), + wireGuardObfuscation: .init(state: .quic) + ) + + XCTAssertNoThrow( + try wrapper + .selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) + ) + + settings = LatestTunnelSettings( + relayConstraints: .init(port: .only(1)), + wireGuardObfuscation: .init(state: .udpOverTcp) + ) + + XCTAssertNoThrow( + try wrapper + .selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) + ) + + settings = LatestTunnelSettings( + relayConstraints: .init(port: .only(1)), + wireGuardObfuscation: .init(state: .shadowsocks) + ) + + XCTAssertNoThrow( + try wrapper + .selectRelays(tunnelSettings: settings, connectionAttemptCount: 0) + ) + } } diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift index 9a1039dfcc..c2996c6741 100644 --- a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift +++ b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift @@ -131,32 +131,32 @@ class TunnelControlPage: Page { /// Verify that the app attempts to connect over UDP before switching to TCP. For testing blocked UDP traffic. @discardableResult func verifyConnectingOverTCPAfterUDPAttempts() -> Self { - let connectionAttempts = waitForConnectionAttempts(4, timeout: 30) + let connectionAttempts = waitForConnectionAttempts(3, timeout: 30) // TODO: Revisit this when QUIC obfuscation is added - // Should do four connection attempts but due to UI bug sometimes only two are displayed, sometimes all four - if connectionAttempts.count == 4 { // Expected retries flow + // Should do three connection attempts but due to UI bug sometimes only two are displayed, sometimes all three + if connectionAttempts.count == 3 { // Expected retries flow for (attemptIndex, attempt) in connectionAttempts.enumerated() { - if attemptIndex < 3 { + if attemptIndex < 2 { XCTAssertEqual(attempt.protocolName, "UDP") - } else if attemptIndex == 3 { + } else if attemptIndex == 2 { XCTAssertEqual(attempt.protocolName, "TCP") } else { XCTFail("Unexpected connection attempt") } } - } else if connectionAttempts.count == 3 { // Most of the times this incorrect flow is shown + } else if connectionAttempts.count == 2 { // Most of the times this incorrect flow is shown for (attemptIndex, attempt) in connectionAttempts.enumerated() { - if attemptIndex == 0 || attemptIndex == 1 { + if attemptIndex == 0 { XCTAssertEqual(attempt.protocolName, "UDP") - } else if attemptIndex == 2 { + } else if attemptIndex == 1 { XCTAssertEqual(attempt.protocolName, "TCP") } else { XCTFail("Unexpected connection attempt") } } } else { - XCTFail("Unexpected number of connection attempts, expected 3~4, got \(connectionAttempts.count)") + XCTFail("Unexpected number of connection attempts, expected 2~3, got \(connectionAttempts.count)") } return self @@ -164,9 +164,9 @@ class TunnelControlPage: Page { /// Verify that connection attempts are made in the correct order @discardableResult func verifyConnectionAttemptsOrder() -> Self { - var connectionAttempts = waitForConnectionAttempts(4, timeout: 80) + var connectionAttempts = waitForConnectionAttempts(3, timeout: 80) var totalAttemptsOffset = 0 - XCTAssertEqual(connectionAttempts.count, 4) + XCTAssertEqual(connectionAttempts.count, 3) /// Sometimes, the UI will only show an IP address for the first connection attempt, which gets skipped by /// `waitForConnectionAttempts`, and offsets expected attempts count by 1, but still counts towards @@ -177,7 +177,7 @@ class TunnelControlPage: Page { totalAttemptsOffset = 1 } for (attemptIndex, attempt) in connectionAttempts.enumerated() { - if attemptIndex < 3 - totalAttemptsOffset { + if attemptIndex < 2 - totalAttemptsOffset { XCTAssertEqual(attempt.protocolName, "UDP") } else { XCTAssertEqual(attempt.protocolName, "TCP") diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift index 219e7d518d..2383142ba1 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift @@ -58,6 +58,8 @@ public struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol { .noRelaysSatisfyingDaitaConstraints case .noObfuscatedRelaysFound: .noRelaysSatisfyingObfuscationSettings + case .invalidPort: + .noRelaysSatisfyingPortConstraints default: .noRelaysSatisfyingConstraints } diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift index b42a52458e..2421f498a1 100644 --- a/ios/PacketTunnelCore/Actor/State+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift @@ -206,7 +206,8 @@ extension BlockedStateReason { case .noRelaysSatisfyingConstraints, .noRelaysSatisfyingFilterConstraints, .multihopEntryEqualsExit, .noRelaysSatisfyingObfuscationSettings, .noRelaysSatisfyingDaitaConstraints, .readSettings, .invalidAccount, .accountExpired, .deviceRevoked, - .unknown, .deviceLoggedOut, .outdatedSchema, .invalidRelayPublicKey: + .unknown, .deviceLoggedOut, .outdatedSchema, .invalidRelayPublicKey, + .noRelaysSatisfyingPortConstraints: return false } } diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 3aac312490..6f837fc734 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -211,9 +211,12 @@ public enum BlockedStateReason: String, Codable, Equatable, Sendable { /// No relays satisfying DAITA constraints. case noRelaysSatisfyingDaitaConstraints - /// No relays satisfying DAITA constraints. + /// No relays satisfying obfuscation settings. case noRelaysSatisfyingObfuscationSettings + /// No relays satisfying port constraints. + case noRelaysSatisfyingPortConstraints + /// Any other failure when reading settings. case readSettings diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift index 318a451a7b..ba506a0519 100644 --- a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift +++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift @@ -57,13 +57,13 @@ final class ProtocolObfuscatorTests: XCTestCase { func testObfuscateAutomatic() throws { let settings = settings(.automatic, obfuscationPort: .automatic) - try (UInt(0) ... 3).forEach { attempt in + try (UInt(0) ... 2).forEach { attempt in let obfuscated = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: attempt) switch attempt { - case 0, 1: + case 0: XCTAssertEqual(endpoint, obfuscated.endpoint) - case 2, 3: + case 1, 2: let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) validate(obfuscated.endpoint, against: obfuscationProtocol) default: |
