From d1124ecc964512eba478481ea7d6e6cf8bbd7bf0 Mon Sep 17 00:00:00 2001 From: mojganii Date: Mon, 27 May 2024 17:39:02 +0200 Subject: [PATCH] Upgrade settings schema to associate with multi-hop --- .../NoRelaysSatisfyingConstraintsError.swift | 15 + .../Relay/RelaySelector+Shadowsocks.swift | 91 +++++ .../Relay/RelaySelector+Wireguard.swift | 78 ++++ ios/MullvadREST/Relay/RelaySelector.swift | 345 ++++++------------ .../Relay/RelaySelectorResult.swift | 18 + ios/MullvadREST/Relay/RelayWithDistance.swift | 13 + ios/MullvadREST/Relay/RelayWithLocation.swift | 31 ++ .../Shadowsocks/ShadowsocksLoader.swift | 26 +- .../ShadowsocksRelaySelector.swift | 60 +++ ios/MullvadSettings/MultihopSettings.swift | 33 ++ ios/MullvadSettings/TunnelSettings.swift | 12 +- .../TunnelSettingsUpdate.swift | 4 + ios/MullvadSettings/TunnelSettingsV4.swift | 8 +- ios/MullvadSettings/TunnelSettingsV5.swift | 46 +++ ios/MullvadTypes/RelayConstraints.swift | 37 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 42 ++- ios/MullvadVPN/AppDelegate.swift | 15 +- .../ListCustomListCoordinator.swift | 11 +- .../Coordinators/LocationCoordinator.swift | 5 +- .../SimulatorTunnelProviderHost.swift | 9 +- .../TunnelManager/TunnelManager.swift | 6 +- .../Relay/RelaySelectorTests.swift | 128 ++++--- .../MigrationManagerTests.swift | 43 ++- .../TunnelSettingsUpdateTests.swift | 2 +- .../MullvadTypes/RelayConstraintsTests.swift | 2 +- .../PacketTunnelProvider.swift | 21 +- .../RelaySelectorWrapper.swift | 55 ++- .../PacketTunnelProvider/SettingsReader.swift | 3 +- .../Actor/ObservedState.swift | 8 +- .../Actor/PacketTunnelActor.swift | 6 +- .../Protocols/SettingsReaderProtocol.swift | 7 +- ios/PacketTunnelCore/Actor/State.swift | 3 + .../AppMessageHandlerTests.swift | 8 +- .../Mocks/SettingsReaderStub.swift | 3 +- .../PacketTunnelActorTests.swift | 3 +- .../ProtocolObfuscatorTests.swift | 4 +- 36 files changed, 829 insertions(+), 372 deletions(-) create mode 100644 ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift create mode 100644 ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift create mode 100644 ios/MullvadREST/Relay/RelaySelector+Wireguard.swift create mode 100644 ios/MullvadREST/Relay/RelaySelectorResult.swift create mode 100644 ios/MullvadREST/Relay/RelayWithDistance.swift create mode 100644 ios/MullvadREST/Relay/RelayWithLocation.swift create mode 100644 ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift create mode 100644 ios/MullvadSettings/MultihopSettings.swift create mode 100644 ios/MullvadSettings/TunnelSettingsV5.swift diff --git a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift new file mode 100644 index 000000000000..9435929db687 --- /dev/null +++ b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift @@ -0,0 +1,15 @@ +// +// NoRelaysSatisfyingConstraintsError.swift +// MullvadREST +// +// Created by Mojgan on 2024-04-26. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public struct NoRelaysSatisfyingConstraintsError: LocalizedError { + public var errorDescription: String? { + "No relays satisfying constraints." + } +} diff --git a/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift new file mode 100644 index 000000000000..273b9afe03cc --- /dev/null +++ b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift @@ -0,0 +1,91 @@ +// +// RelaySelector+Shadowsocks.swift +// MullvadREST +// +// Created by Mojgan on 2024-05-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +extension RelaySelector { + public enum Shadowsocks { + /** + Returns random shadowsocks TCP bridge, otherwise `nil` if there are no shadowdsocks bridges. + */ + public static func tcpBridge(from relays: REST.ServerRelaysResponse) -> REST.ServerShadowsocks? { + relays.bridge.shadowsocks.filter { $0.protocol == "tcp" }.randomElement() + } + + /// Return a random Shadowsocks bridge relay, or `nil` if no relay were found. + /// + /// Non `active` relays are filtered out. + /// - Parameter relays: The list of relays to randomly select from. + /// - Returns: A Shadowsocks relay or `nil` if no active relay were found. + public static func relay(from relaysResponse: REST.ServerRelaysResponse) -> REST.BridgeRelay? { + relaysResponse.bridge.relays.filter { $0.active }.randomElement() + } + + /// Returns the closest Shadowsocks relay using the given `location`, or a random relay if `constraints` were + /// unsatisfiable. + /// + /// - Parameters: + /// - location: The user selected `location` + /// - port: The user selected port + /// - filter: The user filtered criteria + /// - relays: The list of relays to randomly select from. + /// - Returns: A Shadowsocks relay or `nil` if no active relay were found. + public static func closestRelay( + location: RelayConstraint, + port: RelayConstraint, + filter: RelayConstraint, + in relaysResponse: REST.ServerRelaysResponse + ) -> REST.BridgeRelay? { + let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations) + let filteredRelays = applyConstraints( + location, + portConstraint: port, + filterConstraint: filter, + relays: mappedBridges + ) + guard filteredRelays.isEmpty == false else { return relay(from: relaysResponse) } + + // Compute the midpoint location from all the filtered relays + // Take *either* the first five relays, OR the relays below maximum bridge distance + // sort all of them by Haversine distance from the computed midpoint location + // then use the roulette selection to pick a bridge + + let midpointDistance = Midpoint.location(in: filteredRelays.map { $0.serverLocation.geoCoordinate }) + let maximumBridgeDistance = 1500.0 + let relaysWithDistance = filteredRelays.map { + RelayWithDistance( + relay: $0.relay, + distance: Haversine.distance( + midpointDistance.latitude, + midpointDistance.longitude, + $0.serverLocation.latitude, + $0.serverLocation.longitude + ) + ) + }.sorted { + $0.distance < $1.distance + }.filter { + $0.distance <= maximumBridgeDistance + }.prefix(5) + + var greatestDistance = 0.0 + relaysWithDistance.forEach { + if $0.distance > greatestDistance { + greatestDistance = $0.distance + } + } + + let randomRelay = rouletteSelection(relays: Array(relaysWithDistance), weightFunction: { relay in + UInt64(1 + greatestDistance - relay.distance) + }) + + return randomRelay?.relay ?? filteredRelays.randomElement()?.relay + } + } +} diff --git a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift new file mode 100644 index 000000000000..4607838ac292 --- /dev/null +++ b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift @@ -0,0 +1,78 @@ +// +// RelaySelector+Wireguard.swift +// MullvadREST +// +// Created by Mojgan on 2024-05-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +extension RelaySelector { + public enum WireGuard { + /** + Filters relay list using given constraints and selects random relay for exit relay. + Throws an error if there are no relays satisfying the given constraints. + */ + public static func evaluate( + by constraints: RelayConstraints, + in relaysResponse: REST.ServerRelaysResponse, + numberOfFailedAttempts: UInt + ) throws -> RelaySelectorResult { + let exitCandidates = try findBestMatch( + relays: relaysResponse, + relayConstraint: constraints.exitLocations, + portConstraint: constraints.port, + filterConstraint: constraints.filter, + numberOfFailedAttempts: numberOfFailedAttempts + ) + + return exitCandidates + } + + // MARK: - private functions + + private static func findBestMatch( + relays: REST.ServerRelaysResponse, + relayConstraint: RelayConstraint, + portConstraint: RelayConstraint, + filterConstraint: RelayConstraint, + numberOfFailedAttempts: UInt + ) throws -> RelaySelectorMatch { + let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations) + let filteredRelays = applyConstraints( + relayConstraint, + portConstraint: portConstraint, + filterConstraint: filterConstraint, + relays: mappedRelays + ) + let port = applyPortConstraint( + portConstraint, + rawPortRanges: relays.wireguard.portRanges, + numberOfFailedAttempts: numberOfFailedAttempts + ) + + guard let relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else { + throw NoRelaysSatisfyingConstraintsError() + } + + let endpoint = MullvadEndpoint( + ipv4Relay: IPv4Endpoint( + ip: relayWithLocation.relay.ipv4AddrIn, + port: port + ), + ipv6Relay: nil, + ipv4Gateway: relays.wireguard.ipv4Gateway, + ipv6Gateway: relays.wireguard.ipv6Gateway, + publicKey: relayWithLocation.relay.publicKey + ) + + return RelaySelectorMatch( + endpoint: endpoint, + relay: relayWithLocation.relay, + location: relayWithLocation.serverLocation + ) + } + } +} diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift index 18dce0e83b35..44062134ccc2 100644 --- a/ios/MullvadREST/Relay/RelaySelector.swift +++ b/ios/MullvadREST/Relay/RelaySelector.swift @@ -12,112 +12,7 @@ import MullvadTypes private let defaultPort: UInt16 = 53 public enum RelaySelector { - /** - Returns random shadowsocks TCP bridge, otherwise `nil` if there are no shadowdsocks bridges. - */ - public static func shadowsocksTCPBridge(from relays: REST.ServerRelaysResponse) -> REST.ServerShadowsocks? { - relays.bridge.shadowsocks.filter { $0.protocol == "tcp" }.randomElement() - } - - /// Return a random Shadowsocks bridge relay, or `nil` if no relay were found. - /// - /// Non `active` relays are filtered out. - /// - Parameter relays: The list of relays to randomly select from. - /// - Returns: A Shadowsocks relay or `nil` if no active relay were found. - public static func shadowsocksRelay(from relaysResponse: REST.ServerRelaysResponse) -> REST.BridgeRelay? { - relaysResponse.bridge.relays.filter { $0.active }.randomElement() - } - - /// Returns the closest Shadowsocks relay using the given `constraints`, or a random relay if `constraints` were - /// unsatisfiable. - /// - /// - Parameters: - /// - constraints: The user selected `constraints` - /// - relays: The list of relays to randomly select from. - /// - Returns: A Shadowsocks relay or `nil` if no active relay were found. - public static func closestShadowsocksRelayConstrained( - by constraints: RelayConstraints, - in relaysResponse: REST.ServerRelaysResponse - ) -> REST.BridgeRelay? { - let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations) - let filteredRelays = applyConstraints(constraints, relays: mappedBridges) - guard filteredRelays.isEmpty == false else { return shadowsocksRelay(from: relaysResponse) } - - // Compute the midpoint location from all the filtered relays - // Take *either* the first five relays, OR the relays below maximum bridge distance - // sort all of them by Haversine distance from the computed midpoint location - // then use the roulette selection to pick a bridge - - let midpointDistance = Midpoint.location(in: filteredRelays.map { $0.serverLocation.geoCoordinate }) - let maximumBridgeDistance = 1500.0 - let relaysWithDistance = filteredRelays.map { - RelayWithDistance( - relay: $0.relay, - distance: Haversine.distance( - midpointDistance.latitude, - midpointDistance.longitude, - $0.serverLocation.latitude, - $0.serverLocation.longitude - ) - ) - }.sorted { - $0.distance < $1.distance - }.filter { - $0.distance <= maximumBridgeDistance - }.prefix(5) - - var greatestDistance = 0.0 - relaysWithDistance.forEach { - if $0.distance > greatestDistance { - greatestDistance = $0.distance - } - } - - let randomRelay = rouletteSelection(relays: Array(relaysWithDistance), weightFunction: { relay in - UInt64(1 + greatestDistance - relay.distance) - }) - - return randomRelay?.relay ?? filteredRelays.randomElement()?.relay - } - - /** - Filters relay list using given constraints and selects random relay. - Throws an error if there are no relays satisfying the given constraints. - */ - public static func evaluate( - relays: REST.ServerRelaysResponse, - constraints: RelayConstraints, - numberOfFailedAttempts: UInt - ) throws -> RelaySelectorResult { - let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations) - let filteredRelays = applyConstraints(constraints, relays: mappedRelays) - let port = applyConstraints( - constraints, - rawPortRanges: relays.wireguard.portRanges, - numberOfFailedAttempts: numberOfFailedAttempts - ) - - guard let relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else { - throw NoRelaysSatisfyingConstraintsError() - } - - let endpoint = MullvadEndpoint( - ipv4Relay: IPv4Endpoint( - ip: relayWithLocation.relay.ipv4AddrIn, - port: port - ), - ipv6Relay: nil, - ipv4Gateway: relays.wireguard.ipv4Gateway, - ipv6Gateway: relays.wireguard.ipv6Gateway, - publicKey: relayWithLocation.relay.publicKey - ) - - return RelaySelectorResult( - endpoint: endpoint, - relay: relayWithLocation.relay, - location: relayWithLocation.serverLocation - ) - } + // MARK: - public /// Determines whether a `REST.ServerRelay` satisfies the given relay filter. public static func relayMatchesFilter(_ relay: AnyRelay, filter: RelayFilter) -> Bool { @@ -135,86 +30,37 @@ public enum RelaySelector { } } - /// Produce a list of `RelayWithLocation` items satisfying the given constraints - static func applyConstraints( - _ constraints: RelayConstraints, - relays: [RelayWithLocation] - ) -> [RelayWithLocation] { - // Filter on active status, filter, and location. - let filteredRelays = relays.filter { relayWithLocation -> Bool in - guard relayWithLocation.relay.active else { - return false - } + // MARK: - private - switch constraints.filter { - case .any: - break - case let .only(filter): - if !relayMatchesFilter(relayWithLocation.relay, filter: filter) { - return false - } - } + static func pickRandomRelayByWeight(relays: [RelayWithLocation]) + -> RelayWithLocation? { + rouletteSelection(relays: relays, weightFunction: { relayWithLocation in relayWithLocation.relay.weight }) + } - return switch constraints.locations { - case .any: - true - case let .only(relayConstraint): - // At least one location must match the relay under test. - relayConstraint.locations.contains { location in - relayWithLocation.matches(location: location) - } - } + private static func pickRandomPort(rawPortRanges: [[UInt16]]) -> UInt16? { + let portRanges = parseRawPortRanges(rawPortRanges) + let portAmount = portRanges.reduce(0) { partialResult, closedRange in + partialResult + closedRange.count } - // Filter on country inclusion. - let includeInCountryFilteredRelays = filteredRelays.filter { relayWithLocation in - return switch constraints.locations { - case .any: - true - case let .only(relayConstraint): - relayConstraint.locations.contains { location in - if case .country = location { - return relayWithLocation.relay.includeInCountry - } - return false - } - } + guard var portIndex = (0 ..< portAmount).randomElement() else { + return nil } - // If no relays should be included in the matched country, instead accept all. - if includeInCountryFilteredRelays.isEmpty { - return filteredRelays - } else { - return includeInCountryFilteredRelays + for range in portRanges { + if portIndex < range.count { + return UInt16(portIndex) + range.lowerBound + } else { + portIndex -= range.count + } } - } - - /// Produce a port that is either user provided or randomly selected, satisfying the given constraints. - private static func applyConstraints( - _ constraints: RelayConstraints, - rawPortRanges: [[UInt16]], - numberOfFailedAttempts: UInt - ) -> UInt16? { - switch constraints.port { - case let .only(port): - return port - case .any: - // 1. First two attempts should pick a random port. - // 2. The next two should pick port 53. - // 3. Repeat steps 1 and 2. - let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3) - - return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges) - } - } + assertionFailure("Port selection algorithm is broken!") - private static func pickRandomRelayByWeight(relays: [RelayWithLocation]) - -> RelayWithLocation? { - rouletteSelection(relays: relays, weightFunction: { relayWithLocation in relayWithLocation.relay.weight }) + return nil } - private static func rouletteSelection(relays: [T], weightFunction: (T) -> UInt64) -> T? { + static func rouletteSelection(relays: [T], weightFunction: (T) -> UInt64) -> T? { let totalWeight = relays.map { weightFunction($0) }.reduce(0) { accumulated, weight in accumulated + weight } @@ -241,45 +87,7 @@ public enum RelaySelector { return randomRelay } - private static func pickRandomPort(rawPortRanges: [[UInt16]]) -> UInt16? { - let portRanges = parseRawPortRanges(rawPortRanges) - let portAmount = portRanges.reduce(0) { partialResult, closedRange in - partialResult + closedRange.count - } - - guard var portIndex = (0 ..< portAmount).randomElement() else { - return nil - } - - for range in portRanges { - if portIndex < range.count { - return UInt16(portIndex) + range.lowerBound - } else { - portIndex -= range.count - } - } - - assertionFailure("Port selection algorithm is broken!") - - return nil - } - - private static func parseRawPortRanges(_ rawPortRanges: [[UInt16]]) -> [ClosedRange] { - rawPortRanges.compactMap { inputRange -> ClosedRange? in - guard inputRange.count == 2 else { return nil } - - let startPort = inputRange[0] - let endPort = inputRange[1] - - if startPort <= endPort { - return startPort ... endPort - } else { - return nil - } - } - } - - private static func mapRelays( + static func mapRelays( relays: [T], locations: [String: REST.ServerLocation] ) -> [RelayWithLocation] { @@ -307,42 +115,95 @@ public enum RelaySelector { return RelayWithLocation(relay: relay, serverLocation: location) } -} -public struct NoRelaysSatisfyingConstraintsError: LocalizedError { - public var errorDescription: String? { - "No relays satisfying constraints." + private static func parseRawPortRanges(_ rawPortRanges: [[UInt16]]) -> [ClosedRange] { + rawPortRanges.compactMap { inputRange -> ClosedRange? in + guard inputRange.count == 2 else { return nil } + + let startPort = inputRange[0] + let endPort = inputRange[1] + + if startPort <= endPort { + return startPort ... endPort + } else { + return nil + } + } } -} -public struct RelaySelectorResult: Codable, Equatable { - public var endpoint: MullvadEndpoint - public var relay: REST.ServerRelay - public var location: Location -} + /// Produce a list of `RelayWithLocation` items satisfying the given constraints + static func applyConstraints( + _ relayConstraint: RelayConstraint, + portConstraint: RelayConstraint, + filterConstraint: RelayConstraint, + relays: [RelayWithLocation] + ) -> [RelayWithLocation] { + // Filter on active status, filter, and location. + let filteredRelays = relays.filter { relayWithLocation -> Bool in + guard relayWithLocation.relay.active else { + return false + } -struct RelayWithLocation { - let relay: T - let serverLocation: Location + switch filterConstraint { + case .any: + break + case let .only(filter): + if !relayMatchesFilter(relayWithLocation.relay, filter: filter) { + return false + } + } - func matches(location: RelayLocation) -> Bool { - return switch location { - case let .country(countryCode): - serverLocation.countryCode == countryCode + return switch relayConstraint { + case .any: + true + case let .only(relayConstraint): + // At least one location must match the relay under test. + relayConstraint.locations.contains { location in + relayWithLocation.matches(location: location) + } + } + } - case let .city(countryCode, cityCode): - serverLocation.countryCode == countryCode && - serverLocation.cityCode == cityCode + // Filter on country inclusion. + let includeInCountryFilteredRelays = filteredRelays.filter { relayWithLocation in + return switch relayConstraint { + case .any: + true + case let .only(relayConstraint): + relayConstraint.locations.contains { location in + if case .country = location { + return relayWithLocation.relay.includeInCountry + } + return false + } + } + } - case let .hostname(countryCode, cityCode, hostname): - serverLocation.countryCode == countryCode && - serverLocation.cityCode == cityCode && - relay.hostname == hostname + // If no relays should be included in the matched country, instead accept all. + if includeInCountryFilteredRelays.isEmpty { + return filteredRelays + } else { + return includeInCountryFilteredRelays } } -} -private struct RelayWithDistance { - let relay: T - let distance: Double + /// Produce a port that is either user provided or randomly selected, satisfying the given constraints. + static func applyPortConstraint( + _ portConstraint: RelayConstraint, + rawPortRanges: [[UInt16]], + numberOfFailedAttempts: UInt + ) -> UInt16? { + switch portConstraint { + case let .only(port): + return port + + case .any: + // 1. First two attempts should pick a random port. + // 2. The next two should pick port 53. + // 3. Repeat steps 1 and 2. + let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3) + + return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges) + } + } } diff --git a/ios/MullvadREST/Relay/RelaySelectorResult.swift b/ios/MullvadREST/Relay/RelaySelectorResult.swift new file mode 100644 index 000000000000..3e9ffa4bb013 --- /dev/null +++ b/ios/MullvadREST/Relay/RelaySelectorResult.swift @@ -0,0 +1,18 @@ +// +// RelaySelectorResult.swift +// MullvadREST +// +// Created by Mojgan on 2024-05-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +public typealias RelaySelectorResult = RelaySelectorMatch + +public struct RelaySelectorMatch: Codable, Equatable { + public var endpoint: MullvadEndpoint + public var relay: REST.ServerRelay + public var location: Location +} diff --git a/ios/MullvadREST/Relay/RelayWithDistance.swift b/ios/MullvadREST/Relay/RelayWithDistance.swift new file mode 100644 index 000000000000..aadf5fd565c0 --- /dev/null +++ b/ios/MullvadREST/Relay/RelayWithDistance.swift @@ -0,0 +1,13 @@ +// +// RelayWithDistance.swift +// MullvadREST +// +// Created by Mojgan on 2024-05-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +struct RelayWithDistance { + let relay: T + let distance: Double +} diff --git a/ios/MullvadREST/Relay/RelayWithLocation.swift b/ios/MullvadREST/Relay/RelayWithLocation.swift new file mode 100644 index 000000000000..c80cc34a3a7d --- /dev/null +++ b/ios/MullvadREST/Relay/RelayWithLocation.swift @@ -0,0 +1,31 @@ +// +// RelayWithLocation.swift +// MullvadREST +// +// Created by Mojgan on 2024-05-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +struct RelayWithLocation { + let relay: T + let serverLocation: Location + + func matches(location: RelayLocation) -> Bool { + return switch location { + case let .country(countryCode): + serverLocation.countryCode == countryCode + + case let .city(countryCode, cityCode): + serverLocation.countryCode == countryCode && + serverLocation.cityCode == cityCode + + case let .hostname(countryCode, cityCode, hostname): + serverLocation.countryCode == countryCode && + serverLocation.cityCode == cityCode && + relay.hostname == hostname + } + } +} diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift index 839e3b524fa5..945084847fe0 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadSettings import MullvadTypes public protocol ShadowsocksLoaderProtocol { @@ -15,20 +16,23 @@ public protocol ShadowsocksLoaderProtocol { } public class ShadowsocksLoader: ShadowsocksLoaderProtocol { - private let shadowsocksCache: ShadowsocksConfigurationCache - private let relayCache: RelayCacheProtocol + let shadowsocksCache: ShadowsocksConfigurationCache + let shadowsocksRelaySelector: ShadowsocksRelaySelectorProtocol + let constraintsUpdater: RelayConstraintsUpdater + private var relayConstraints = RelayConstraints() - private let constraintsUpdater: RelayConstraintsUpdater public init( shadowsocksCache: ShadowsocksConfigurationCache, - relayCache: RelayCacheProtocol, + shadowsocksRelaySelector: ShadowsocksRelaySelectorProtocol, constraintsUpdater: RelayConstraintsUpdater ) { self.shadowsocksCache = shadowsocksCache - self.relayCache = relayCache + self.shadowsocksRelaySelector = shadowsocksRelaySelector self.constraintsUpdater = constraintsUpdater + constraintsUpdater.onNewConstraints = { [weak self] newConstraints in + try? self?.reloadConfiguration() self?.relayConstraints = newConstraints } } @@ -45,7 +49,6 @@ public class ShadowsocksLoader: ShadowsocksLoaderProtocol { return try shadowsocksCache.read() } catch { // There is no previous configuration either if this is the first time this code ran - // Or because the previous shadowsocks configuration was invalid, therefore generate a new one. let newConfiguration = try create() try shadowsocksCache.write(newConfiguration) return newConfiguration @@ -54,14 +57,11 @@ public class ShadowsocksLoader: ShadowsocksLoaderProtocol { /// Returns a randomly selected shadowsocks configuration. private func create() throws -> ShadowsocksConfiguration { - let cachedRelays = try relayCache.read() - let bridgeConfiguration = RelaySelector.shadowsocksTCPBridge(from: cachedRelays.relays) - let closestRelay = RelaySelector.closestShadowsocksRelayConstrained( - by: relayConstraints, - in: cachedRelays.relays - ) + let bridgeConfiguration = try shadowsocksRelaySelector.getBridges() + let closestRelay = try shadowsocksRelaySelector.selectRelay(with: relayConstraints) - guard let bridgeAddress = closestRelay?.ipv4AddrIn, let bridgeConfiguration else { throw POSIXError(.ENOENT) } + guard let bridgeAddress = closestRelay?.ipv4AddrIn, + let bridgeConfiguration else { throw POSIXError(.ENOENT) } return ShadowsocksConfiguration( address: .ipv4(bridgeAddress), diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift new file mode 100644 index 000000000000..2ee6b172feab --- /dev/null +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift @@ -0,0 +1,60 @@ +// +// ShadowsocksRelaySelector.swift +// MullvadREST +// +// Created by Mojgan on 2024-05-23. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadSettings +import MullvadTypes + +public protocol ShadowsocksRelaySelectorProtocol { + func selectRelay( + with constraints: RelayConstraints + ) throws -> REST.BridgeRelay? + + func getBridges() throws -> REST.ServerShadowsocks? +} + +final public class ShadowsocksRelaySelector: ShadowsocksRelaySelectorProtocol { + let relayCache: RelayCacheProtocol + let multihopStateUpdater: MultihopStateUpdater + private var multihopState: MultihopState = .off + + public init( + relayCache: RelayCacheProtocol, + multihopStateUpdater: MultihopStateUpdater + ) { + self.relayCache = relayCache + self.multihopStateUpdater = multihopStateUpdater + + multihopStateUpdater.onNewState = { [weak self] newState in + self?.multihopState = newState + } + } + + public func selectRelay( + with constraints: RelayConstraints + ) throws -> REST.BridgeRelay? { + let cachedRelays = try relayCache.read().relays + + let locationConstraint = switch multihopState { + case .on: constraints.entryLocations + case .off: constraints.exitLocations + } + + return RelaySelector.Shadowsocks.closestRelay( + location: locationConstraint, + port: constraints.port, + filter: constraints.filter, + in: cachedRelays + ) + } + + public func getBridges() throws -> REST.ServerShadowsocks? { + let cachedRelays = try relayCache.read() + return RelaySelector.Shadowsocks.tcpBridge(from: cachedRelays.relays) + } +} diff --git a/ios/MullvadSettings/MultihopSettings.swift b/ios/MullvadSettings/MultihopSettings.swift new file mode 100644 index 000000000000..cbff1ab70aac --- /dev/null +++ b/ios/MullvadSettings/MultihopSettings.swift @@ -0,0 +1,33 @@ +// +// MultihopSettings.swift +// MullvadSettings +// +// Created by Mojgan on 2024-04-26. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public protocol MultihopStatePropagation { + var onNewState: ((MultihopState) -> Void)? { get set } +} + +public class MultihopStateUpdater: MultihopStatePropagation { + public var onNewState: ((MultihopState) -> Void)? + + public init(onNewState: ((MultihopState) -> Void)? = nil) { + self.onNewState = onNewState + } +} + +/// Whether Multi-hop is enabled +public enum MultihopState: Codable { + case on + case off +} + +public extension MultihopState { + var isEnabled: Bool { + self == .on + } +} diff --git a/ios/MullvadSettings/TunnelSettings.swift b/ios/MullvadSettings/TunnelSettings.swift index b8a276cb7cbc..853e3dc70ee3 100644 --- a/ios/MullvadSettings/TunnelSettings.swift +++ b/ios/MullvadSettings/TunnelSettings.swift @@ -9,7 +9,7 @@ import Foundation /// Alias to the latest version of the `TunnelSettings`. -public typealias LatestTunnelSettings = TunnelSettingsV4 +public typealias LatestTunnelSettings = TunnelSettingsV5 /// Protocol all TunnelSettings must adhere to, for upgrade purposes. public protocol TunnelSettings: Codable { @@ -27,14 +27,19 @@ public enum SchemaVersion: Int, Equatable { /// V2 format with WireGuard obfuscation options, stored as `TunnelSettingsV3`. case v3 = 3 + /// V3 format with post quantum options, stored as `TunnelSettingsV4`. case v4 = 4 + /// V4 format with multi-hop options, stored as `TunnelSettingsV5`. + case v5 = 5 + var settingsType: any TunnelSettings.Type { switch self { case .v1: return TunnelSettingsV1.self case .v2: return TunnelSettingsV2.self case .v3: return TunnelSettingsV3.self case .v4: return TunnelSettingsV4.self + case .v5: return TunnelSettingsV5.self } } @@ -43,10 +48,11 @@ public enum SchemaVersion: Int, Equatable { case .v1: return .v2 case .v2: return .v3 case .v3: return .v4 - case .v4: return .v4 + case .v4: return .v5 + case .v5: return .v5 } } /// Current schema version. - public static let current = SchemaVersion.v4 + public static let current = SchemaVersion.v5 } diff --git a/ios/MullvadSettings/TunnelSettingsUpdate.swift b/ios/MullvadSettings/TunnelSettingsUpdate.swift index c915e5dc7f5f..92349a38ed79 100644 --- a/ios/MullvadSettings/TunnelSettingsUpdate.swift +++ b/ios/MullvadSettings/TunnelSettingsUpdate.swift @@ -14,6 +14,7 @@ public enum TunnelSettingsUpdate { case obfuscation(WireGuardObfuscationSettings) case relayConstraints(RelayConstraints) case quantumResistance(TunnelQuantumResistance) + case multihop(MultihopState) } extension TunnelSettingsUpdate { @@ -27,6 +28,8 @@ extension TunnelSettingsUpdate { settings.relayConstraints = newRelayConstraints case let .quantumResistance(newQuantumResistance): settings.tunnelQuantumResistance = newQuantumResistance + case let .multihop(newState): + settings.tunnelMultihopState = newState } } @@ -36,6 +39,7 @@ extension TunnelSettingsUpdate { case .obfuscation: "obfuscation settings" case .relayConstraints: "relay constraints" case .quantumResistance: "quantum resistance" + case .multihop: "Multihop" } } } diff --git a/ios/MullvadSettings/TunnelSettingsV4.swift b/ios/MullvadSettings/TunnelSettingsV4.swift index 0d938bc27974..9b75a7ebf37e 100644 --- a/ios/MullvadSettings/TunnelSettingsV4.swift +++ b/ios/MullvadSettings/TunnelSettingsV4.swift @@ -35,6 +35,12 @@ public struct TunnelSettingsV4: Codable, Equatable, TunnelSettings { } public func upgradeToNextVersion() -> any TunnelSettings { - self + TunnelSettingsV5( + relayConstraints: relayConstraints, + dnsSettings: dnsSettings, + wireGuardObfuscation: wireGuardObfuscation, + tunnelQuantumResistance: tunnelQuantumResistance, + tunnelMultihopState: .off + ) } } diff --git a/ios/MullvadSettings/TunnelSettingsV5.swift b/ios/MullvadSettings/TunnelSettingsV5.swift new file mode 100644 index 000000000000..e8035d0b6629 --- /dev/null +++ b/ios/MullvadSettings/TunnelSettingsV5.swift @@ -0,0 +1,46 @@ +// +// TunnelSettingsV5.swift +// MullvadSettings +// +// Created by Mojgan on 2024-05-13. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +public struct TunnelSettingsV5: Codable, Equatable, TunnelSettings { + /// Relay constraints. + public var relayConstraints: RelayConstraints + + /// DNS settings. + public var dnsSettings: DNSSettings + + /// WireGuard obfuscation settings + public var wireGuardObfuscation: WireGuardObfuscationSettings + + /// Whether Post Quantum exchanges are enabled. + public var tunnelQuantumResistance: TunnelQuantumResistance + + /// Whether Multi-hop is enabled. + public var tunnelMultihopState: MultihopState + + public init( + relayConstraints: RelayConstraints = RelayConstraints(), + dnsSettings: DNSSettings = DNSSettings(), + wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings(), + tunnelQuantumResistance: TunnelQuantumResistance = .automatic, + tunnelMultihopState: MultihopState = .off + + ) { + self.relayConstraints = relayConstraints + self.dnsSettings = dnsSettings + self.wireGuardObfuscation = wireGuardObfuscation + self.tunnelQuantumResistance = tunnelQuantumResistance + self.tunnelMultihopState = tunnelMultihopState + } + + public func upgradeToNextVersion() -> any TunnelSettings { + self + } +} diff --git a/ios/MullvadTypes/RelayConstraints.swift b/ios/MullvadTypes/RelayConstraints.swift index 21444a26586f..125d81b1c162 100644 --- a/ios/MullvadTypes/RelayConstraints.swift +++ b/ios/MullvadTypes/RelayConstraints.swift @@ -24,24 +24,31 @@ public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible @available(*, deprecated, renamed: "locations") private var location: RelayConstraint = .only(.country("se")) + // Added in 2024.1 + // Changed from RelayLocations to UserSelectedRelays in 2024.3 + @available(*, deprecated, renamed: "exitLocations") + private var locations: RelayConstraint = .only(UserSelectedRelays(locations: [.country("se")])) + + // Added in 2024.5 to support multi-hop + public var entryLocations: RelayConstraint + public var exitLocations: RelayConstraint + // Added in 2023.3 public var port: RelayConstraint public var filter: RelayConstraint - // Added in 2024.1 - // Changed from RelayLocations to UserSelectedRelays in 2024.3 - public var locations: RelayConstraint - public var debugDescription: String { - "RelayConstraints { locations: \(locations), port: \(port), filter: \(filter) }" + "RelayConstraints { entry locations: \(entryLocations), exit locations: \(exitLocations) , port: \(port), filter: \(filter) }" } public init( - locations: RelayConstraint = .only(UserSelectedRelays(locations: [.country("se")])), + entryLocations: RelayConstraint = .only(UserSelectedRelays(locations: [.country("se")])), + exitLocations: RelayConstraint = .only(UserSelectedRelays(locations: [.country("se")])), port: RelayConstraint = .any, filter: RelayConstraint = .any ) { - self.locations = locations + self.entryLocations = entryLocations + self.exitLocations = exitLocations self.port = port self.filter = filter } @@ -53,9 +60,19 @@ public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible port = try container.decodeIfPresent(RelayConstraint.self, forKey: .port) ?? .any filter = try container.decodeIfPresent(RelayConstraint.self, forKey: .filter) ?? .any - // Added in 2024.1 - locations = try container.decodeIfPresent(RelayConstraint.self, forKey: .locations) - ?? Self.migrateRelayLocation(decoder: decoder) + // Added in 2024.5 + entryLocations = try container.decodeIfPresent( + RelayConstraint.self, + forKey: .entryLocations + ) ?? .only(UserSelectedRelays(locations: [.country("se")])) + + exitLocations = try container + .decodeIfPresent(RelayConstraint.self, forKey: .exitLocations) ?? + container.decodeIfPresent( + RelayConstraint.self, + forKey: .locations + ) ?? + Self.migrateRelayLocation(decoder: decoder) ?? .only(UserSelectedRelays(locations: [.country("se")])) } } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index e0f774528d45..9bc64f529a74 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -841,6 +841,7 @@ E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; }; F006CCFC2B99CC8400C6C2AC /* EditLocationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */; }; + F01528BB2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */; }; F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */; }; F0164EBC2B482E430020268D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBB2B482E430020268D /* AppStorage.swift */; }; F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; }; @@ -906,6 +907,10 @@ F0ACE3362BE517D6006D5333 /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */; }; F0ACE3372BE517F1006D5333 /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */; }; F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */; }; + F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */; }; + F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F02BF751E300817A42 /* RelayWithDistance.swift */; }; + F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */; }; + F0B894F52BF7528700817A42 /* RelaySelector+Shadowsocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */; }; F0BE65372B9F136A005CC385 /* LocationSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */; }; F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; F0C3333C2B31A29C00D1A478 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; @@ -926,6 +931,8 @@ F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4282B220A15006B57A7 /* RelaySelector.swift */; }; F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4292B220A15006B57A7 /* Midpoint.swift */; }; F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */; }; + F0E61CAA2BF2911D000C4A95 /* TunnelSettingsV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E61CA82BF2911D000C4A95 /* TunnelSettingsV5.swift */; }; + F0E61CAB2BF2911D000C4A95 /* MultihopSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */; }; F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */; }; F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */; }; F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedController.swift */; }; @@ -936,6 +943,8 @@ F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */; }; F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */; }; F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */; }; + F0F316192BF3572B0078DBCF /* RelaySelectorResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */; }; + F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */; }; F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; F0FADDEB2BE90AAE000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; F0FADDEC2BE90AB0000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; @@ -2077,6 +2086,7 @@ E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = ""; }; E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = ""; }; F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationsCoordinator.swift; sourceTree = ""; }; + F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksRelaySelector.swift; sourceTree = ""; }; F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryStub.swift; sourceTree = ""; }; F0164EBB2B482E430020268D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = ""; }; @@ -2121,6 +2131,10 @@ F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProxyFactory.swift; sourceTree = ""; }; F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ServerRelaysResponse+Stubs.swift"; sourceTree = ""; }; F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = ""; }; + F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = ""; }; + F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = ""; }; + F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Wireguard.swift"; sourceTree = ""; }; + F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Shadowsocks.swift"; sourceTree = ""; }; F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationSectionHeaderView.swift; sourceTree = ""; }; F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = ""; }; F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = ""; }; @@ -2137,6 +2151,8 @@ F0DDE4282B220A15006B57A7 /* RelaySelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = ""; }; F0DDE4292B220A15006B57A7 /* Midpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Midpoint.swift; sourceTree = ""; }; F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = ""; }; + F0E61CA82BF2911D000C4A95 /* TunnelSettingsV5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV5.swift; sourceTree = ""; }; + F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultihopSettings.swift; sourceTree = ""; }; F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = ""; }; F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedController.swift; sourceTree = ""; }; @@ -2148,6 +2164,8 @@ F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeLogInteractor.swift; sourceTree = ""; }; F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogViewModel.swift; sourceTree = ""; }; F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = ""; }; + F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = ""; }; + F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2636,13 +2654,13 @@ 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, 58B93A1226C3F13600A55733 /* TunnelState.swift */, 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */, + A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */, 5803B4B12940A48700C23744 /* TunnelStore.swift */, + A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */, 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */, 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */, 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */, - A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */, - A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */, ); path = TunnelManager; sourceTree = ""; @@ -3286,6 +3304,7 @@ 068CE5732927B7A400A068BB /* Migration.swift */, A9D96B192A8247C100A5C673 /* MigrationManager.swift */, 58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */, + F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */, 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */, 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */, 58FF2C02281BDE02009EF542 /* SettingsManager.swift */, @@ -3296,11 +3315,12 @@ A92ECC272A7802AB0052F1B1 /* StoredDeviceData.swift */, A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */, A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */, + 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */, 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */, 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */, A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */, A93181A02B727ED700E341D2 /* TunnelSettingsV4.swift */, - 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */, + F0E61CA82BF2911D000C4A95 /* TunnelSettingsV5.swift */, A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */, ); path = MullvadSettings; @@ -4072,8 +4092,14 @@ F0DDE4272B220A15006B57A7 /* Haversine.swift */, 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */, F0DDE4292B220A15006B57A7 /* Midpoint.swift */, + F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, + F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, + F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */, + F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */, + F0B894F02BF751E300817A42 /* RelayWithDistance.swift */, + F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */, ); path = Relay; sourceTree = ""; @@ -4112,6 +4138,7 @@ F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */, F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */, F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */, + F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */, F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */, ); path = Shadowsocks; @@ -5155,8 +5182,10 @@ buildActionMask = 2147483647; files = ( F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */, + F0F316192BF3572B0078DBCF /* RelaySelectorResult.swift in Sources */, F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */, A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */, + F01528BB2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift in Sources */, 06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */, A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */, A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */, @@ -5165,6 +5194,7 @@ A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */, 06799AEC28F98E4800ACD94E /* RESTTaskIdentifier.swift in Sources */, 58E7BA192A975DF70068EC3A /* RESTTransportProvider.swift in Sources */, + F0B894F52BF7528700817A42 /* RelaySelector+Shadowsocks.swift in Sources */, 06799ADE28F98E4800ACD94E /* RESTRequestHandler.swift in Sources */, F0DDE4162B220458006B57A7 /* TransportProvider.swift in Sources */, 06799AEF28F98E4800ACD94E /* RetryStrategy.swift in Sources */, @@ -5189,12 +5219,15 @@ 06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */, A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */, 06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */, + F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */, 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */, F0DDE4142B220458006B57A7 /* ShadowSocksProxy.swift in Sources */, A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */, F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */, 06799AE628F98E4800ACD94E /* ServerRelaysResponse.swift in Sources */, F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */, + F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */, + F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */, F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */, A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */, 06799AF128F98E4800ACD94E /* RESTAPIProxy.swift in Sources */, @@ -5205,6 +5238,7 @@ F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */, A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */, A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */, + F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */, 06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */, A90763B72B2857D50045ADF0 /* Socks5DataStreamHandler.swift in Sources */, A90763B22B2857D50045ADF0 /* Socks5EndpointReader.swift in Sources */, @@ -5407,6 +5441,7 @@ buildActionMask = 2147483647; files = ( F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */, + F0E61CAA2BF2911D000C4A95 /* TunnelSettingsV5.swift in Sources */, 7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */, 58B2FDEE2AA72098003EB5C6 /* ApplicationConfiguration.swift in Sources */, F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */, @@ -5437,6 +5472,7 @@ 58B2FDE22AA71D5C003EB5C6 /* StoredAccountData.swift in Sources */, F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */, 7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */, + F0E61CAB2BF2911D000C4A95 /* MultihopSettings.swift in Sources */, 58B2FDE82AA71D5C003EB5C6 /* KeychainSettingsStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index b31acdcaab3d..c43500a4cc30 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -40,7 +40,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private(set) var relayCacheTracker: RelayCacheTracker! private(set) var storePaymentManager: StorePaymentManager! private var transportMonitor: TransportMonitor! - private var relayConstraintsObserver: TunnelBlockObserver! + private var settingsObserver: TunnelBlockObserver! private let migrationManager = MigrationManager() private(set) var accessMethodRepository = AccessMethodRepository() @@ -90,10 +90,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD tunnelManager = createTunnelManager(application: application) let constraintsUpdater = RelayConstraintsUpdater() - relayConstraintsObserver = TunnelBlockObserver(didUpdateTunnelSettings: { _, settings in + let multihopUpdater = MultihopStateUpdater() + settingsObserver = TunnelBlockObserver(didUpdateTunnelSettings: { _, settings in + multihopUpdater.onNewState?(settings.tunnelMultihopState) constraintsUpdater.onNewConstraints?(settings.relayConstraints) }) - tunnelManager.addObserver(relayConstraintsObserver) + tunnelManager.addObserver(settingsObserver) storePaymentManager = StorePaymentManager( backgroundTaskProvider: application, @@ -102,13 +104,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy: accountsProxy, transactionLog: .default ) - let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession()) let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) + let shadowsocksRelaySelector = ShadowsocksRelaySelector( + relayCache: ipOverrideWrapper, + multihopStateUpdater: multihopUpdater + ) shadowsocksLoader = ShadowsocksLoader( shadowsocksCache: shadowsocksCache, - relayCache: ipOverrideWrapper, + shadowsocksRelaySelector: shadowsocksRelaySelector, constraintsUpdater: constraintsUpdater ) diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift index 713458e5b540..ff69c6288788 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift @@ -78,29 +78,30 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting { private func updateRelayConstraints(for action: EditCustomListCoordinator.FinishAction, in list: CustomList) { var relayConstraints = tunnelManager.settings.relayConstraints - guard let customListSelection = relayConstraints.locations.value?.customListSelection, + guard let customListSelection = relayConstraints.exitLocations.value?.customListSelection, customListSelection.listId == list.id else { return } switch action { case .save: + // TODO: - Add entry locations if customListSelection.isList { let selectedRelays = UserSelectedRelays( locations: list.locations, customListSelection: UserSelectedRelays.CustomListSelection(listId: list.id, isList: true) ) - relayConstraints.locations = .only(selectedRelays) + relayConstraints.exitLocations = .only(selectedRelays) } else { let selectedConstraintIsRemovedFromList = list.locations.filter { - relayConstraints.locations.value?.locations.contains($0) ?? false + relayConstraints.exitLocations.value?.locations.contains($0) ?? false }.isEmpty if selectedConstraintIsRemovedFromList { - relayConstraints.locations = .only(UserSelectedRelays(locations: [])) + relayConstraints.exitLocations = .only(UserSelectedRelays(locations: [])) } } case .delete: - relayConstraints.locations = .only(UserSelectedRelays(locations: [])) + relayConstraints.exitLocations = .only(UserSelectedRelays(locations: [])) } tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { [weak self] in diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift index 58e828ef1289..38b9ced8cd22 100644 --- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift @@ -54,9 +54,10 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { } func start() { + // TODO: - the location should be defined whether it's Entry or Exit location let locationViewControllerWrapper = LocationViewControllerWrapper( customListRepository: customListRepository, - selectedRelays: tunnelManager.settings.relayConstraints.locations.value + selectedRelays: tunnelManager.settings.relayConstraints.exitLocations.value ) locationViewControllerWrapper.delegate = self @@ -156,7 +157,7 @@ extension LocationCoordinator: RelayCacheTrackerObserver { extension LocationCoordinator: LocationViewControllerWrapperDelegate { func didSelectRelays(relays: UserSelectedRelays) { var relayConstraints = tunnelManager.settings.relayConstraints - relayConstraints.locations = .only(relays) + relayConstraints.exitLocations = .only(relays) tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { self.tunnelManager.startTunnel() diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index ea5260b8af2d..ce9bbfc0f6fb 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -159,9 +159,9 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { private func pickRelay() throws -> SelectedRelay { let cachedRelays = try relayCacheTracker.getCachedRelays() let tunnelSettings = try SettingsManager.readSettings() - let selectorResult = try RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: tunnelSettings.relayConstraints, + let selectorResult = try RelaySelector.WireGuard.evaluate( + by: tunnelSettings.relayConstraints, + in: cachedRelays.relays, numberOfFailedAttempts: 0 ) return SelectedRelay( @@ -185,7 +185,8 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { connectionAttemptCount: 0, transportLayer: .udp, remotePort: selectedRelay.endpoint.ipv4Relay.port, - isPostQuantum: settings.tunnelQuantumResistance.isEnabled + isPostQuantum: settings.tunnelQuantumResistance.isEnabled, + isMultihop: settings.tunnelMultihopState.isEnabled ) ) } catch { diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index f16281019912..bced82fd46db 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -782,9 +782,9 @@ final class TunnelManager: StorePaymentObserver { fileprivate func selectRelay() throws -> SelectedRelay { let cachedRelays = try relayCacheTracker.getCachedRelays() let retryAttempts = tunnelStatus.observedState.connectionState?.connectionAttemptCount ?? 0 - let selectorResult = try RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: settings.relayConstraints, + let selectorResult = try RelaySelector.WireGuard.evaluate( + by: settings.relayConstraints, + in: cachedRelays.relays, numberOfFailedAttempts: retryAttempts ) diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift index 68f48c011289..9163613bbe5e 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift @@ -17,14 +17,16 @@ private let defaultPort: UInt16 = 53 class RelaySelectorTests: XCTestCase { let sampleRelays = ServerRelaysResponseStubs.sampleRelays + // MARK: - single-Hop tests + func testCountryConstraint() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.country("es")])) + exitLocations: .only(UserSelectedRelays(locations: [.country("es")])) ) - let result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) @@ -33,26 +35,25 @@ class RelaySelectorTests: XCTestCase { func testCityConstraint() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.city("se", "got")])) + exitLocations: .only(UserSelectedRelays(locations: [.city("se", "got")])) ) - let result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) - XCTAssertEqual(result.relay.hostname, "se10-wireguard") } func testHostnameConstraint() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) ) - let result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) @@ -61,7 +62,7 @@ class RelaySelectorTests: XCTestCase { func testMultipleLocationsConstraint() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [ + exitLocations: .only(UserSelectedRelays(locations: [ .city("se", "got"), .hostname("se", "sto", "se6-wireguard"), ])) @@ -84,13 +85,19 @@ class RelaySelectorTests: XCTestCase { ) } - let constrainedLocations = RelaySelector.applyConstraints(constraints, relays: relayWithLocations) + let constrainedLocations = RelaySelector.applyConstraints( + constraints.exitLocations, + portConstraint: constraints.port, + filterConstraint: constraints.filter, + relays: relayWithLocations + ) XCTAssertTrue( constrainedLocations.contains( where: { $0.matches(location: .city("se", "got")) } ) ) + XCTAssertTrue( constrainedLocations.contains( where: { $0.matches(location: .hostname("se", "sto", "se6-wireguard")) } @@ -100,13 +107,13 @@ class RelaySelectorTests: XCTestCase { func testSpecificPortConstraint() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), port: .only(1) ) - let result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) @@ -115,47 +122,70 @@ class RelaySelectorTests: XCTestCase { func testRandomPortSelectionWithFailedAttempts() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) ) let allPorts = portRanges.flatMap { $0 } - var result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + var result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port)) - result = try RelaySelector.evaluate(relays: sampleRelays, constraints: constraints, numberOfFailedAttempts: 1) + result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, + numberOfFailedAttempts: 1 + ) XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port)) - result = try RelaySelector.evaluate(relays: sampleRelays, constraints: constraints, numberOfFailedAttempts: 2) + result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, + numberOfFailedAttempts: 2 + ) XCTAssertEqual(result.endpoint.ipv4Relay.port, defaultPort) - result = try RelaySelector.evaluate(relays: sampleRelays, constraints: constraints, numberOfFailedAttempts: 3) + result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, + numberOfFailedAttempts: 3 + ) XCTAssertEqual(result.endpoint.ipv4Relay.port, defaultPort) - result = try RelaySelector.evaluate(relays: sampleRelays, constraints: constraints, numberOfFailedAttempts: 4) + result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, + numberOfFailedAttempts: 4 + ) XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port)) } func testClosestShadowsocksRelay() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.city("se", "sto")])) + exitLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])) ) - let selectedRelay = RelaySelector.closestShadowsocksRelayConstrained(by: constraints, in: sampleRelays) + let selectedRelay = RelaySelector.Shadowsocks.closestRelay( + location: constraints.exitLocations, + port: constraints.port, + filter: constraints.filter, + in: sampleRelays + ) XCTAssertEqual(selectedRelay?.hostname, "se-sto-br-001") } func testClosestShadowsocksRelayIsRandomWhenNoContraintsAreSatisfied() throws { let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.country("INVALID COUNTRY")])) + exitLocations: .only(UserSelectedRelays(locations: [.country("INVALID COUNTRY")])) ) - let selectedRelay = try XCTUnwrap(RelaySelector.closestShadowsocksRelayConstrained( - by: constraints, + let selectedRelay = try XCTUnwrap(RelaySelector.Shadowsocks.closestRelay( + location: constraints.exitLocations, + port: constraints.port, + filter: constraints.filter, in: sampleRelays )) @@ -166,13 +196,13 @@ class RelaySelectorTests: XCTestCase { let filter = RelayFilter(ownership: .owned, providers: .any) let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), filter: .only(filter) ) - let result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) @@ -183,13 +213,13 @@ class RelaySelectorTests: XCTestCase { let filter = RelayFilter(ownership: .rented, providers: .any) let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), filter: .only(filter) ) - let result = try? RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try? RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) @@ -201,13 +231,13 @@ class RelaySelectorTests: XCTestCase { let filter = RelayFilter(ownership: .any, providers: .only([provider])) let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), filter: .only(filter) ) - let result = try RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) @@ -219,16 +249,18 @@ class RelaySelectorTests: XCTestCase { let filter = RelayFilter(ownership: .any, providers: .only([provider])) let constraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])), filter: .only(filter) ) - let result = try? RelaySelector.evaluate( - relays: sampleRelays, - constraints: constraints, + let result = try? RelaySelector.WireGuard.evaluate( + by: constraints, + in: sampleRelays, numberOfFailedAttempts: 0 ) XCTAssertNil(result) } + + // MARK: - Multi-Hop tests } diff --git a/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift b/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift index ace14e8eb0a6..22be6900f2cb 100644 --- a/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift +++ b/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift @@ -119,10 +119,49 @@ final class MigrationManagerTests: XCTestCase { wait(for: [failedMigrationExpectation], timeout: 1) } + func testSuccessfulMigrationFromV4ToLatest() throws { + var settingsV4 = TunnelSettingsV4() + let relayConstraints = RelayConstraints( + exitLocations: .only(UserSelectedRelays(locations: [.city("jp", "osa")])) + ) + + settingsV4.relayConstraints = relayConstraints + settingsV4.tunnelQuantumResistance = .off + settingsV4.wireGuardObfuscation = WireGuardObfuscationSettings(state: .off, port: .automatic) + + try migrateToLatest(settingsV4, version: .v4) + + // Once the migration is done, settings should have been updated to the latest available version + // Verify that the old settings are still valid + let latestSettings = try SettingsManager.readSettings() + XCTAssertEqual(settingsV4.relayConstraints, latestSettings.relayConstraints) + XCTAssertEqual(settingsV4.tunnelQuantumResistance, latestSettings.tunnelQuantumResistance) + XCTAssertEqual(settingsV4.wireGuardObfuscation, latestSettings.wireGuardObfuscation) + } + + func testSuccessfulMigrationFromV3ToLatest() throws { + var settingsV3 = TunnelSettingsV3() + let relayConstraints = RelayConstraints( + exitLocations: .only(UserSelectedRelays(locations: [.city("jp", "osa")])) + ) + + settingsV3.relayConstraints = relayConstraints + settingsV3.dnsSettings = DNSSettings() + settingsV3.wireGuardObfuscation = WireGuardObfuscationSettings(state: .on, port: .port80) + + try migrateToLatest(settingsV3, version: .v3) + + // Once the migration is done, settings should have been updated to the latest available version + // Verify that the old settings are still valid + let latestSettings = try SettingsManager.readSettings() + XCTAssertEqual(settingsV3.relayConstraints, latestSettings.relayConstraints) + XCTAssertEqual(settingsV3.wireGuardObfuscation, latestSettings.wireGuardObfuscation) + } + func testSuccessfulMigrationFromV2ToLatest() throws { var settingsV2 = TunnelSettingsV2() let osakaRelayConstraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.city("jp", "osa")])) + exitLocations: .only(UserSelectedRelays(locations: [.city("jp", "osa")])) ) settingsV2.relayConstraints = osakaRelayConstraints @@ -136,7 +175,7 @@ final class MigrationManagerTests: XCTestCase { func testSuccessfulMigrationFromV1ToLatest() throws { var settingsV1 = TunnelSettingsV1() let osakaRelayConstraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.city("jp", "osa")])) + exitLocations: .only(UserSelectedRelays(locations: [.city("jp", "osa")])) ) settingsV1.relayConstraints = osakaRelayConstraints diff --git a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift index 89a234f3ce9a..346dce00b77d 100644 --- a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift +++ b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift @@ -48,7 +48,7 @@ final class TunnelSettingsUpdateTests: XCTestCase { // When: let relayConstraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.country("zz")])), + exitLocations: .only(UserSelectedRelays(locations: [.country("zz")])), port: .only(9999), filter: .only(.init(ownership: .rented, providers: .only(["foo", "bar"]))) ) diff --git a/ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift b/ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift index 401dc13eddb2..15ed22663e28 100644 --- a/ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift +++ b/ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift @@ -20,7 +20,7 @@ final class RelayConstraintsTests: XCTestCase { let constraintsFromJson = try parseData(from: constraintsV1) let constraintsFromInit = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.city("se", "got")])), + exitLocations: .only(UserSelectedRelays(locations: [.city("se", "got")])), port: .only(80), filter: .only(RelayFilter(ownership: .rented, providers: .any)) ) diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 6b27aa59f83d..cd071ce7ed10 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -21,6 +21,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue") private let providerLogger: Logger private let constraintsUpdater = RelayConstraintsUpdater() + private let multihopUpdater = MultihopStateUpdater() + private let settingsReader = SettingsReader() private var actor: PacketTunnelActor! private var postQuantumActor: PostQuantumKeyExchangeActor! @@ -69,7 +71,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let devicesProxy = proxyFactory.createDevicesProxy() deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy) - relaySelector = RelaySelectorWrapper(relayCache: ipOverrideWrapper) + relaySelector = RelaySelectorWrapper( + relayCache: ipOverrideWrapper, + multihopState: .off, + multihopStateUpdater: multihopUpdater + ) actor = PacketTunnelActor( timings: PacketTunnelActorTimings(), @@ -78,7 +84,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue), blockedStateErrorMapper: BlockedStateErrorMapper(), relaySelector: relaySelector, - settingsReader: SettingsReader(), + settingsReader: settingsReader, protocolObfuscator: ProtocolObfuscator() ) @@ -156,12 +162,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let urlSession = REST.makeURLSession() let urlSessionTransport = URLSessionTransport(urlSession: urlSession) let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: appContainerURL) + let settings = try? settingsReader.read() + + let shadowsocksRelaySelector = ShadowsocksRelaySelector( + relayCache: ipOverrideWrapper, + multihopStateUpdater: multihopUpdater + ) let transportStrategy = TransportStrategy( datasource: AccessMethodRepository(), shadowsocksLoader: ShadowsocksLoader( shadowsocksCache: shadowsocksCache, - relayCache: ipOverrideWrapper, + shadowsocksRelaySelector: shadowsocksRelaySelector, constraintsUpdater: constraintsUpdater ) ) @@ -251,6 +263,9 @@ extension PacketTunnelProvider { // Cache last connection attempt to filter out repeating calls. lastConnectionAttempt = connectionAttempt + // Pass multi-hop state retrieved during the last read from setting into relay selector. + multihopUpdater.onNewState?(connState.isMultihop ? .on : .off) + case let .negotiatingPostQuantumKey(_, privateKey): postQuantumActor.endCurrentNegotiation() postQuantumActor.startNegotiation(with: privateKey) diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift index 73f6fa267407..704dbf81f7b9 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift @@ -8,27 +8,56 @@ import Foundation import MullvadREST +import MullvadSettings import MullvadTypes import PacketTunnelCore -struct RelaySelectorWrapper: RelaySelectorProtocol { +struct MultihopNotImplementedError: LocalizedError { + public var errorDescription: String? { + "Picking relays for Multihop is not implemented yet." + } +} + +final class RelaySelectorWrapper: RelaySelectorProtocol { let relayCache: RelayCacheProtocol + let multihopStateUpdater: MultihopStateUpdater + private var multihopState: MultihopState = .off + + public init( + relayCache: RelayCacheProtocol, + multihopState: MultihopState, + multihopStateUpdater: MultihopStateUpdater + ) { + self.relayCache = relayCache + self.multihopState = multihopState + self.multihopStateUpdater = multihopStateUpdater + + multihopStateUpdater.onNewState = { [weak self] newState in + self?.multihopState = newState + } + } func selectRelay( with constraints: RelayConstraints, connectionAttemptFailureCount: UInt ) throws -> SelectedRelay { - let selectorResult = try RelaySelector.evaluate( - relays: relayCache.read().relays, - constraints: constraints, - numberOfFailedAttempts: connectionAttemptFailureCount - ) - - return SelectedRelay( - endpoint: selectorResult.endpoint, - hostname: selectorResult.relay.hostname, - location: selectorResult.location, - retryAttempts: connectionAttemptFailureCount - ) + switch multihopState { + case .off: + let selectorResult = try RelaySelector.WireGuard.evaluate( + by: constraints, + in: relayCache.read().relays, + numberOfFailedAttempts: connectionAttemptFailureCount + ) + + return SelectedRelay( + endpoint: selectorResult.endpoint, + hostname: selectorResult.relay.hostname, + location: selectorResult.location, + retryAttempts: connectionAttemptFailureCount + ) + + case .on: + throw MultihopNotImplementedError() + } } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift index ade3b5a3afd2..2de052e1bd69 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift @@ -22,7 +22,8 @@ struct SettingsReader: SettingsReaderProtocol { relayConstraints: settings.relayConstraints, dnsServers: settings.dnsSettings.selectedDNSServers, obfuscation: settings.wireGuardObfuscation, - quantumResistance: settings.tunnelQuantumResistance + quantumResistance: settings.tunnelQuantumResistance, + multihopState: settings.tunnelMultihopState ) } } diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index bdb85a8e51b4..215348fd7955 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -34,6 +34,7 @@ public struct ObservedConnectionState: Equatable, Codable { public var remotePort: UInt16 public var lastKeyRotation: Date? public let isPostQuantum: Bool + public let isMultihop: Bool public var isNetworkReachable: Bool { networkReachability != .unreachable @@ -47,7 +48,8 @@ public struct ObservedConnectionState: Equatable, Codable { transportLayer: TransportLayer, remotePort: UInt16, lastKeyRotation: Date? = nil, - isPostQuantum: Bool + isPostQuantum: Bool, + isMultihop: Bool ) { self.selectedRelay = selectedRelay self.relayConstraints = relayConstraints @@ -57,6 +59,7 @@ public struct ObservedConnectionState: Equatable, Codable { self.remotePort = remotePort self.lastKeyRotation = lastKeyRotation self.isPostQuantum = isPostQuantum + self.isMultihop = isMultihop } } @@ -101,7 +104,8 @@ extension State.ConnectionData { transportLayer: transportLayer, remotePort: remotePort, lastKeyRotation: lastKeyRotation, - isPostQuantum: isPostQuantum + isPostQuantum: isPostQuantum, + isMultihop: isMultihop ) } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 153c1c043058..dc3bdabc4d46 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -376,7 +376,8 @@ extension PacketTunnelActor { connectedEndpoint: selectedRelay.endpoint, transportLayer: .udp, remotePort: selectedRelay.endpoint.ipv4Relay.port, - isPostQuantum: settings.quantumResistance.isEnabled + isPostQuantum: settings.quantumResistance.isEnabled, + isMultihop: settings.multihopState.isEnabled ) } @@ -415,7 +416,8 @@ extension PacketTunnelActor { connectedEndpoint: obfuscatedEndpoint, transportLayer: transportLayer, remotePort: protocolObfuscator.remotePort, - isPostQuantum: settings.quantumResistance.isEnabled + isPostQuantum: settings.quantumResistance.isEnabled, + isMultihop: settings.multihopState.isEnabled ) } diff --git a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift index ffe7cdcc2ffe..05d60a23f2b2 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift @@ -42,13 +42,17 @@ public struct Settings { public var quantumResistance: TunnelQuantumResistance + /// Whether multi-hop is enabled. + public var multihopState: MultihopState + public init( privateKey: PrivateKey, interfaceAddresses: [IPAddressRange], relayConstraints: RelayConstraints, dnsServers: SelectedDNSServers, obfuscation: WireGuardObfuscationSettings, - quantumResistance: TunnelQuantumResistance + quantumResistance: TunnelQuantumResistance, + multihopState: MultihopState ) { self.privateKey = privateKey self.interfaceAddresses = interfaceAddresses @@ -56,6 +60,7 @@ public struct Settings { self.dnsServers = dnsServers self.obfuscation = obfuscation self.quantumResistance = quantumResistance + self.multihopState = multihopState } } diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index f99799201cea..316202140eb7 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -149,6 +149,9 @@ extension State { /// True if post-quantum key exchange is enabled public let isPostQuantum: Bool + + /// True if multi-hop is enabled + public let isMultihop: Bool } /// Data associated with error state. diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift index cdd062b098f5..37664e68532f 100644 --- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -79,11 +79,11 @@ final class AppMessageHandlerTests: XCTestCase { let appMessageHandler = createAppMessageHandler(actor: actor) let relayConstraints = RelayConstraints( - locations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])) ) - let selectorResult = try XCTUnwrap(try? RelaySelector.evaluate( - relays: ServerRelaysResponseStubs.sampleRelays, - constraints: relayConstraints, + let selectorResult = try XCTUnwrap(try? RelaySelector.WireGuard.evaluate( + by: relayConstraints, + in: ServerRelaysResponseStubs.sampleRelays, numberOfFailedAttempts: 0 )) diff --git a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift index edb9e99e6d40..f5806b9a47f2 100644 --- a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift @@ -30,7 +30,8 @@ extension SettingsReaderStub { relayConstraints: RelayConstraints(), dnsServers: .gateway, obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic), - quantumResistance: .automatic + quantumResistance: .automatic, + multihopState: .off ) return SettingsReaderStub { diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index 9fa8b90258fd..277854ea8fea 100644 --- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -209,7 +209,8 @@ final class PacketTunnelActorTests: XCTestCase { relayConstraints: RelayConstraints(), dnsServers: .gateway, obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic), - quantumResistance: .automatic + quantumResistance: .automatic, + multihopState: .off ) } } diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift index 21f30991bbf5..e81ae10bf606 100644 --- a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift +++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift @@ -111,7 +111,9 @@ final class ProtocolObfuscatorTests: XCTestCase { obfuscation: WireGuardObfuscationSettings( state: obfuscationState, port: obfuscationPort - ), quantumResistance: quantumResistance + ), + quantumResistance: quantumResistance, + multihopState: .off ) } }