summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-06-04 11:44:54 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-06-04 11:44:54 +0200
commitafcbb674ba15292b6ce7c48b61a9e9318bc159c0 (patch)
tree74e9fff31783b2e20d68fa1c832d8834253e86ef
parent5fc0b40c3b6089a126bee6fa2c16d0844163aa4a (diff)
parent7a57bb390287d21ad3ca12cc15c1529c9aacc56a (diff)
downloadmullvadvpn-afcbb674ba15292b6ce7c48b61a9e9318bc159c0.tar.xz
mullvadvpn-afcbb674ba15292b6ce7c48b61a9e9318bc159c0.zip
Merge branch 'upgrade-settings-schema-to-associate-with-multi-hop-ios-689'
-rw-r--r--ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift15
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift91
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Wireguard.swift78
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift345
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorResult.swift18
-rw-r--r--ios/MullvadREST/Relay/RelayWithDistance.swift13
-rw-r--r--ios/MullvadREST/Relay/RelayWithLocation.swift31
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift16
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift46
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift71
-rw-r--r--ios/MullvadREST/Transport/TransportStrategy.swift2
-rw-r--r--ios/MullvadRESTTests/Mocks/MemoryCache.swift2
-rw-r--r--ios/MullvadRESTTests/ShadowsocksLoaderStub.swift2
-rw-r--r--ios/MullvadSettings/MultihopSettings.swift72
-rw-r--r--ios/MullvadSettings/TunnelSettings.swift12
-rw-r--r--ios/MullvadSettings/TunnelSettingsUpdate.swift4
-rw-r--r--ios/MullvadSettings/TunnelSettingsV4.swift8
-rw-r--r--ios/MullvadSettings/TunnelSettingsV5.swift46
-rw-r--r--ios/MullvadTypes/FileCache.swift8
-rw-r--r--ios/MullvadTypes/ObserverList.swift (renamed from ios/MullvadVPN/Classes/ObserverList.swift)16
-rw-r--r--ios/MullvadTypes/RelayConstraints.swift37
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj66
-rw-r--r--ios/MullvadVPN/AppDelegate.swift21
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift11
-rw-r--r--ios/MullvadVPN/Coordinators/LocationCoordinator.swift5
-rw-r--r--ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift2
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift6
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/Tunnel.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift18
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelStore.swift1
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift128
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift143
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift43
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/MultihopUpdaterTests.swift46
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift14
-rw-r--r--ios/MullvadVPNTests/MullvadTypes/MockFileCache.swift7
-rw-r--r--ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift2
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift28
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift63
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift3
-rw-r--r--ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift7
-rw-r--r--ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift8
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift3
-rw-r--r--ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift3
-rw-r--r--ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift4
46 files changed, 1172 insertions, 404 deletions
diff --git a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
new file mode 100644
index 0000000000..9435929db6
--- /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 0000000000..273b9afe03
--- /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<UserSelectedRelays>,
+ port: RelayConstraint<UInt16>,
+ filter: RelayConstraint<RelayFilter>,
+ 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 0000000000..4607838ac2
--- /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<UserSelectedRelays>,
+ portConstraint: RelayConstraint<UInt16>,
+ filterConstraint: RelayConstraint<RelayFilter>,
+ 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 18dce0e83b..44062134cc 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<T: AnyRelay>(
- _ constraints: RelayConstraints,
- relays: [RelayWithLocation<T>]
- ) -> [RelayWithLocation<T>] {
- // 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<T: AnyRelay>(relays: [RelayWithLocation<T>])
+ -> RelayWithLocation<T>? {
+ 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<T: AnyRelay>(relays: [RelayWithLocation<T>])
- -> RelayWithLocation<T>? {
- rouletteSelection(relays: relays, weightFunction: { relayWithLocation in relayWithLocation.relay.weight })
+ return nil
}
- private static func rouletteSelection<T>(relays: [T], weightFunction: (T) -> UInt64) -> T? {
+ static func rouletteSelection<T>(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<UInt16>] {
- rawPortRanges.compactMap { inputRange -> ClosedRange<UInt16>? 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<T: AnyRelay>(
+ static func mapRelays<T: AnyRelay>(
relays: [T],
locations: [String: REST.ServerLocation]
) -> [RelayWithLocation<T>] {
@@ -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<UInt16>] {
+ rawPortRanges.compactMap { inputRange -> ClosedRange<UInt16>? 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<T: AnyRelay>(
+ _ relayConstraint: RelayConstraint<UserSelectedRelays>,
+ portConstraint: RelayConstraint<UInt16>,
+ filterConstraint: RelayConstraint<RelayFilter>,
+ relays: [RelayWithLocation<T>]
+ ) -> [RelayWithLocation<T>] {
+ // Filter on active status, filter, and location.
+ let filteredRelays = relays.filter { relayWithLocation -> Bool in
+ guard relayWithLocation.relay.active else {
+ return false
+ }
-struct RelayWithLocation<T: AnyRelay> {
- 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<T: AnyRelay> {
- 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<UInt16>,
+ 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 0000000000..3e9ffa4bb0
--- /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 0000000000..aadf5fd565
--- /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<T: AnyRelay> {
+ 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 0000000000..c80cc34a3a
--- /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<T: AnyRelay> {
+ 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/ShadowsocksConfigurationCache.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift
index b09f4afdf6..e5c68b631c 100644
--- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift
+++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift
@@ -9,8 +9,14 @@
import Foundation
import MullvadTypes
+public protocol ShadowsocksConfigurationCacheProtocol {
+ func read() throws -> ShadowsocksConfiguration
+ func write(_ configuration: ShadowsocksConfiguration) throws
+ func clear() throws
+}
+
/// Holds a shadowsocks configuration object backed by a caching mechanism shared across processes
-public final class ShadowsocksConfigurationCache {
+public final class ShadowsocksConfigurationCache: ShadowsocksConfigurationCacheProtocol {
private let configurationLock = NSLock()
private var cachedConfiguration: ShadowsocksConfiguration?
private let fileCache: FileCache<ShadowsocksConfiguration>
@@ -44,4 +50,12 @@ public final class ShadowsocksConfigurationCache {
cachedConfiguration = configuration
try fileCache.write(configuration)
}
+
+ /// Clear cached configuration.
+ public func clear() throws {
+ configurationLock.lock()
+ defer { configurationLock.unlock() }
+ cachedConfiguration = nil
+ try fileCache.clear()
+ }
}
diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
index 839e3b524f..f079fbf860 100644
--- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
+++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
@@ -7,61 +7,63 @@
//
import Foundation
+import MullvadSettings
import MullvadTypes
public protocol ShadowsocksLoaderProtocol {
func load() throws -> ShadowsocksConfiguration
- func reloadConfiguration() throws
+ func clear() throws
}
public class ShadowsocksLoader: ShadowsocksLoaderProtocol {
- private let shadowsocksCache: ShadowsocksConfigurationCache
- private let relayCache: RelayCacheProtocol
+ let cache: ShadowsocksConfigurationCacheProtocol
+ let relaySelector: ShadowsocksRelaySelectorProtocol
+ let constraintsUpdater: RelayConstraintsUpdater
+
private var relayConstraints = RelayConstraints()
- private let constraintsUpdater: RelayConstraintsUpdater
public init(
- shadowsocksCache: ShadowsocksConfigurationCache,
- relayCache: RelayCacheProtocol,
+ cache: ShadowsocksConfigurationCacheProtocol,
+ relaySelector: ShadowsocksRelaySelectorProtocol,
constraintsUpdater: RelayConstraintsUpdater
) {
- self.shadowsocksCache = shadowsocksCache
- self.relayCache = relayCache
+ self.cache = cache
+ self.relaySelector = relaySelector
self.constraintsUpdater = constraintsUpdater
+
+ // The constraints gets updated a lot when observing the tunnel, avoid clearing the cache if the constraints haven't changed.
constraintsUpdater.onNewConstraints = { [weak self] newConstraints in
- self?.relayConstraints = newConstraints
+ if self?.relayConstraints != newConstraints {
+ self?.relayConstraints = newConstraints
+ try? self?.clear()
+ }
}
}
- public func reloadConfiguration() throws {
- let newConfiguration = try create()
- try shadowsocksCache.write(newConfiguration)
+ public func clear() throws {
+ try self.cache.clear()
}
/// Returns the last used shadowsocks configuration, otherwise a new randomized configuration.
public func load() throws -> ShadowsocksConfiguration {
do {
// If a previous shadowsocks configuration was in cache, return it directly.
- return try shadowsocksCache.read()
+ return try cache.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)
+ try cache.write(newConfiguration)
return newConfiguration
}
}
/// 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 relaySelector.getBridges()
+ let closestRelay = try relaySelector.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 0000000000..2c9efa5ca2
--- /dev/null
+++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift
@@ -0,0 +1,71 @@
+//
+// 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 multihopUpdater: MultihopUpdater
+ private var multihopState: MultihopState
+ private var observer: MultihopObserverBlock!
+
+ deinit {
+ self.multihopUpdater.removeObserver(observer)
+ }
+
+ public init(
+ relayCache: RelayCacheProtocol,
+ multihopUpdater: MultihopUpdater,
+ multihopState: MultihopState
+ ) {
+ self.relayCache = relayCache
+ self.multihopUpdater = multihopUpdater
+ self.multihopState = multihopState
+ self.addObserver()
+ }
+
+ private func addObserver() {
+ self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, multihopState in
+ self?.multihopState = multihopState
+ })
+ multihopUpdater.addObserver(observer)
+ }
+
+ 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/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift
index 72920ddcd5..a1d029b2b6 100644
--- a/ios/MullvadREST/Transport/TransportStrategy.swift
+++ b/ios/MullvadREST/Transport/TransportStrategy.swift
@@ -57,7 +57,7 @@ public struct TransportStrategy: Equatable {
let configuration = accessMethodIterator.pick()
switch configuration.kind {
case .bridges:
- try? shadowsocksLoader.reloadConfiguration()
+ try? shadowsocksLoader.clear()
fallthrough
default:
self.accessMethodIterator.rotate()
diff --git a/ios/MullvadRESTTests/Mocks/MemoryCache.swift b/ios/MullvadRESTTests/Mocks/MemoryCache.swift
index f01daa32b7..d0e495f889 100644
--- a/ios/MullvadRESTTests/Mocks/MemoryCache.swift
+++ b/ios/MullvadRESTTests/Mocks/MemoryCache.swift
@@ -18,4 +18,6 @@ struct MemoryCache: FileCacheProtocol {
}
func write(_ content: REST.StoredAddressCache) throws {}
+
+ func clear() throws {}
}
diff --git a/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift b/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift
index 4442ddf63f..973ea954a0 100644
--- a/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift
+++ b/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift
@@ -15,7 +15,7 @@ struct ShadowsocksLoaderStub: ShadowsocksLoaderProtocol {
var configuration: ShadowsocksConfiguration
var error: Error?
- func reloadConfiguration() throws {
+ func clear() throws {
try load()
}
diff --git a/ios/MullvadSettings/MultihopSettings.swift b/ios/MullvadSettings/MultihopSettings.swift
new file mode 100644
index 0000000000..881324e792
--- /dev/null
+++ b/ios/MullvadSettings/MultihopSettings.swift
@@ -0,0 +1,72 @@
+//
+// MultihopSettings.swift
+// MullvadSettings
+//
+// Created by Mojgan on 2024-04-26.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+public protocol MultihopPropagation {
+ typealias MultihopHandler = (MultihopState) -> Void
+ var onNewMultihop: MultihopHandler? { get set }
+}
+
+public protocol MultihopObserver: AnyObject {
+ func multihop(_ object: MultihopPropagation, didUpdateMultihop state: MultihopState)
+}
+
+public class MultihopObserverBlock: MultihopObserver {
+ public typealias DidUpdateMultihopHandler = (MultihopPropagation, MultihopState) -> Void
+ public var onNewState: DidUpdateMultihopHandler
+
+ public init(didUpdateMultihop: @escaping DidUpdateMultihopHandler) {
+ self.onNewState = didUpdateMultihop
+ }
+
+ public func multihop(_ object: MultihopPropagation, didUpdateMultihop state: MultihopState) {
+ self.onNewState(object, state)
+ }
+}
+
+public final class MultihopStateListener: MultihopPropagation {
+ public var onNewMultihop: MultihopHandler?
+
+ public init(onNewMultihop: MultihopHandler? = nil) {
+ self.onNewMultihop = onNewMultihop
+ }
+}
+
+public class MultihopUpdater {
+ /// Observers.
+ private let observerList = ObserverList<MultihopObserver>()
+ private var listener: MultihopPropagation
+
+ public init(listener: MultihopPropagation) {
+ self.listener = listener
+ self.listener.onNewMultihop = { [weak self] state in
+ guard let self else { return }
+ self.observerList.notify {
+ $0.multihop(listener, didUpdateMultihop: state)
+ }
+ }
+ }
+
+ // MARK: - Multihop observations
+
+ public func addObserver(_ observer: MultihopObserver) {
+ observerList.append(observer)
+ }
+
+ public func removeObserver(_ observer: MultihopObserver) {
+ observerList.remove(observer)
+ }
+}
+
+/// Whether Multi-hop is enabled
+public enum MultihopState: Codable {
+ case on
+ case off
+}
diff --git a/ios/MullvadSettings/TunnelSettings.swift b/ios/MullvadSettings/TunnelSettings.swift
index b8a276cb7c..853e3dc70e 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 c915e5dc7f..92349a38ed 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 0d938bc279..9b75a7ebf3 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 0000000000..e8035d0b66
--- /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/FileCache.swift b/ios/MullvadTypes/FileCache.swift
index 381bfb41a4..635d1e9c28 100644
--- a/ios/MullvadTypes/FileCache.swift
+++ b/ios/MullvadTypes/FileCache.swift
@@ -31,6 +31,13 @@ public struct FileCache<Content: Codable>: FileCacheProtocol {
try JSONEncoder().encode(content).write(to: fileURL)
}
}
+
+ public func clear() throws {
+ let fileCoordinator = NSFileCoordinator(filePresenter: nil)
+ try fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forDeleting]) { fileURL in
+ try FileManager.default.removeItem(at: fileURL)
+ }
+ }
}
/// Protocol describing file cache that's able to read and write serializable content.
@@ -39,4 +46,5 @@ public protocol FileCacheProtocol<Content> {
func read() throws -> Content
func write(_ content: Content) throws
+ func clear() throws
}
diff --git a/ios/MullvadVPN/Classes/ObserverList.swift b/ios/MullvadTypes/ObserverList.swift
index 45123d39a4..2f26085c44 100644
--- a/ios/MullvadVPN/Classes/ObserverList.swift
+++ b/ios/MullvadTypes/ObserverList.swift
@@ -8,14 +8,14 @@
import Foundation
-struct WeakBox<T> {
- var value: T? {
+public struct WeakBox<T> {
+ public var value: T? {
valueProvider()
}
private let valueProvider: () -> T?
- init(_ value: T) {
+ public init(_ value: T) {
let reference = value as AnyObject
valueProvider = { [weak reference] in
@@ -28,11 +28,13 @@ struct WeakBox<T> {
}
}
-final class ObserverList<T> {
+final public class ObserverList<T> {
private let lock = NSLock()
private var observers = [WeakBox<T>]()
- func append(_ observer: T) {
+ public init() {}
+
+ public func append(_ observer: T) {
lock.lock()
let hasObserver = observers.contains { box in
@@ -46,7 +48,7 @@ final class ObserverList<T> {
lock.unlock()
}
- func remove(_ observer: T) {
+ public func remove(_ observer: T) {
lock.lock()
let index = observers.firstIndex { box in
@@ -60,7 +62,7 @@ final class ObserverList<T> {
lock.unlock()
}
- func forEach(_ body: (T) -> Void) {
+ public func notify(_ body: (T) -> Void) {
lock.lock()
var indicesToRemove = [Int]()
diff --git a/ios/MullvadTypes/RelayConstraints.swift b/ios/MullvadTypes/RelayConstraints.swift
index 21444a2658..125d81b1c1 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<RelayLocation> = .only(.country("se"))
+ // Added in 2024.1
+ // Changed from RelayLocations to UserSelectedRelays in 2024.3
+ @available(*, deprecated, renamed: "exitLocations")
+ private var locations: RelayConstraint<UserSelectedRelays> = .only(UserSelectedRelays(locations: [.country("se")]))
+
+ // Added in 2024.5 to support multi-hop
+ public var entryLocations: RelayConstraint<UserSelectedRelays>
+ public var exitLocations: RelayConstraint<UserSelectedRelays>
+
// Added in 2023.3
public var port: RelayConstraint<UInt16>
public var filter: RelayConstraint<RelayFilter>
- // Added in 2024.1
- // Changed from RelayLocations to UserSelectedRelays in 2024.3
- public var locations: RelayConstraint<UserSelectedRelays>
-
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<UserSelectedRelays> = .only(UserSelectedRelays(locations: [.country("se")])),
+ entryLocations: RelayConstraint<UserSelectedRelays> = .only(UserSelectedRelays(locations: [.country("se")])),
+ exitLocations: RelayConstraint<UserSelectedRelays> = .only(UserSelectedRelays(locations: [.country("se")])),
port: RelayConstraint<UInt16> = .any,
filter: RelayConstraint<RelayFilter> = .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<UInt16>.self, forKey: .port) ?? .any
filter = try container.decodeIfPresent(RelayConstraint<RelayFilter>.self, forKey: .filter) ?? .any
- // Added in 2024.1
- locations = try container.decodeIfPresent(RelayConstraint<UserSelectedRelays>.self, forKey: .locations)
- ?? Self.migrateRelayLocation(decoder: decoder)
+ // Added in 2024.5
+ entryLocations = try container.decodeIfPresent(
+ RelayConstraint<UserSelectedRelays>.self,
+ forKey: .entryLocations
+ ) ?? .only(UserSelectedRelays(locations: [.country("se")]))
+
+ exitLocations = try container
+ .decodeIfPresent(RelayConstraint<UserSelectedRelays>.self, forKey: .exitLocations) ??
+ container.decodeIfPresent(
+ RelayConstraint<UserSelectedRelays>.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 8ae7c3dd96..13f8a47aad 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -311,7 +311,6 @@
58C9B8CE2ABB252E00040B46 /* DeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */; };
58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */; };
58CAFA032985367600BE19F7 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAFA01298530DC00BE19F7 /* Promise.swift */; };
- 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
58CCA010224249A1004F3011 /* TunnelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA00F224249A1004F3011 /* TunnelViewController.swift */; };
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA01122424D11004F3011 /* SettingsViewController.swift */; };
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA0152242560B004F3011 /* UIColor+Palette.swift */; };
@@ -730,7 +729,6 @@
A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
A9A5F9E62ACB05160083449F /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; };
A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
- A9A5F9E92ACB05160083449F /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */; };
A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; };
@@ -843,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 */; };
@@ -873,6 +872,8 @@
F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E52B231EB700B2D37A /* URLSessionTransport.swift */; };
F06045EA2B23217E00B2D37A /* ShadowsocksTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */; };
F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045EB2B2322A500B2D37A /* Jittered.swift */; };
+ F072D3CF2C07122400906F64 /* MultihopUpdaterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */; };
+ F072D3D22C071AD100906F64 /* ShadowsocksLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */; };
F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; };
F07C9D952B220C77006F1C5E /* libshadowsocks_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */; };
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
@@ -908,6 +909,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 */; };
@@ -928,6 +933,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 */; };
@@ -938,6 +945,9 @@
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 */; };
+ F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; };
F0FADDEC2BE90AB0000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; };
/* End PBXBuildFile section */
@@ -2072,6 +2082,7 @@
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; };
F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationsCoordinator.swift; sourceTree = "<group>"; };
+ F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksRelaySelector.swift; sourceTree = "<group>"; };
F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryStub.swift; sourceTree = "<group>"; };
F0164EBB2B482E430020268D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = "<group>"; };
F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = "<group>"; };
@@ -2098,6 +2109,8 @@
F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; };
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; };
F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; };
+ F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopUpdaterTests.swift; sourceTree = "<group>"; };
+ F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderTests.swift; sourceTree = "<group>"; };
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = "<group>"; };
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
@@ -2116,6 +2129,10 @@
F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProxyFactory.swift; sourceTree = "<group>"; };
F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ServerRelaysResponse+Stubs.swift"; sourceTree = "<group>"; };
F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = "<group>"; };
+ F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = "<group>"; };
+ F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = "<group>"; };
+ F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Wireguard.swift"; sourceTree = "<group>"; };
+ F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Shadowsocks.swift"; sourceTree = "<group>"; };
F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationSectionHeaderView.swift; sourceTree = "<group>"; };
F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; };
F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = "<group>"; };
@@ -2132,6 +2149,8 @@
F0DDE4282B220A15006B57A7 /* RelaySelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; };
F0DDE4292B220A15006B57A7 /* Midpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Midpoint.swift; sourceTree = "<group>"; };
F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = "<group>"; };
+ F0E61CA82BF2911D000C4A95 /* TunnelSettingsV5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV5.swift; sourceTree = "<group>"; };
+ F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultihopSettings.swift; sourceTree = "<group>"; };
F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = "<group>"; };
F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedController.swift; sourceTree = "<group>"; };
@@ -2143,6 +2162,8 @@
F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeLogInteractor.swift; sourceTree = "<group>"; };
F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogViewModel.swift; sourceTree = "<group>"; };
F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = "<group>"; };
+ F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; };
+ F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -2397,6 +2418,7 @@
440E9EF32BDA942E00B1FD11 /* MullvadREST */ = {
isa = PBXGroup;
children = (
+ F072D3D02C071A9100906F64 /* Shadowsocks */,
440E9EF42BDA943B00B1FD11 /* ApiHandlers */,
440E9EF52BDA954000B1FD11 /* Relay */,
);
@@ -2544,6 +2566,7 @@
7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */,
7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */,
A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */,
+ F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */,
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */,
);
path = MullvadSettings;
@@ -2598,6 +2621,7 @@
58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */,
7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */,
A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */,
+ 58CC40EE24A601900019D96E /* ObserverList.swift */,
58CAFA01298530DC00BE19F7 /* Promise.swift */,
449EBA242B975B7C00DFA4EB /* Protocols */,
5898D2B12902A6DE00EB5EBA /* RelayConstraint.swift */,
@@ -2633,13 +2657,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 = "<group>";
@@ -2986,7 +3010,6 @@
582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */,
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */,
58DFF7D12B0256A300F864E0 /* MarkdownStylingOptions.swift */,
- 58CC40EE24A601900019D96E /* ObserverList.swift */,
);
path = Classes;
sourceTree = "<group>";
@@ -3284,6 +3307,7 @@
068CE5732927B7A400A068BB /* Migration.swift */,
A9D96B192A8247C100A5C673 /* MigrationManager.swift */,
58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */,
+ F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */,
586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */,
44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */,
58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
@@ -3294,11 +3318,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;
@@ -4020,6 +4045,14 @@
path = ApiHandlers;
sourceTree = "<group>";
};
+ F072D3D02C071A9100906F64 /* Shadowsocks */ = {
+ isa = PBXGroup;
+ children = (
+ F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */,
+ );
+ path = Shadowsocks;
+ sourceTree = "<group>";
+ };
F09D04B82AE94F27003D4F89 /* GeneralAPIs */ = {
isa = PBXGroup;
children = (
@@ -4069,8 +4102,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 = "<group>";
@@ -4109,6 +4148,7 @@
F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */,
F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */,
F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */,
+ F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */,
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */,
);
path = Shadowsocks;
@@ -5122,8 +5162,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 */,
@@ -5132,6 +5174,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 */,
@@ -5156,12 +5199,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 */,
@@ -5172,6 +5218,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 */,
@@ -5247,7 +5294,6 @@
A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */,
A9A5F9E62ACB05160083449F /* DeviceDataThrottling.swift in Sources */,
A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */,
- A9A5F9E92ACB05160083449F /* ObserverList.swift in Sources */,
A9B6AC1B2ADEA3AD00F7802A /* MemoryCache.swift in Sources */,
7A9BE5A32B8F89B900E2A7D0 /* LocationNode.swift in Sources */,
A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */,
@@ -5270,6 +5316,7 @@
F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */,
7ABFB09E2BA316220074A49E /* RelayConstraintsTests.swift in Sources */,
+ F072D3D22C071AD100906F64 /* ShadowsocksLoaderTests.swift in Sources */,
F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */,
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */,
A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */,
@@ -5310,6 +5357,7 @@
A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */,
A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */,
A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */,
+ F072D3CF2C07122400906F64 /* MultihopUpdaterTests.swift in Sources */,
F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */,
58BE4B9D2B18A85B007EA1D3 /* NSAttributedString+Markdown.swift in Sources */,
A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */,
@@ -5375,6 +5423,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 */,
@@ -5405,6 +5454,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;
@@ -5652,7 +5702,6 @@
7A6389E92B7F8FE2008E77E1 /* CustomListValidationError.swift in Sources */,
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */,
- 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
7A58699F2B50057100640D27 /* AccessMethodKind.swift in Sources */,
7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */,
7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */,
@@ -5946,6 +5995,7 @@
581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */,
449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */,
58D22417294C90210029F5F8 /* FixedWidthInteger+Arithmetics.swift in Sources */,
+ F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index b31acdcaab..645acdeb0f 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,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
tunnelManager = createTunnelManager(application: application)
let constraintsUpdater = RelayConstraintsUpdater()
- relayConstraintsObserver = TunnelBlockObserver(didUpdateTunnelSettings: { _, settings in
+ let multihopListener = MultihopStateListener()
+ let multihopUpdater = MultihopUpdater(listener: multihopListener)
+ let multihopState = (try? SettingsManager.readSettings().tunnelMultihopState) ?? .off
+
+ settingsObserver = TunnelBlockObserver(didUpdateTunnelSettings: { _, settings in
+ multihopListener.onNewMultihop?(settings.tunnelMultihopState)
constraintsUpdater.onNewConstraints?(settings.relayConstraints)
})
- tunnelManager.addObserver(relayConstraintsObserver)
+ tunnelManager.addObserver(settingsObserver)
storePaymentManager = StorePaymentManager(
backgroundTaskProvider: application,
@@ -102,13 +107,17 @@ 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,
+ multihopUpdater: multihopUpdater,
+ multihopState: multihopState
+ )
shadowsocksLoader = ShadowsocksLoader(
- shadowsocksCache: shadowsocksCache,
- relayCache: ipOverrideWrapper,
+ cache: shadowsocksCache,
+ relaySelector: shadowsocksRelaySelector,
constraintsUpdater: constraintsUpdater
)
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
index 713458e5b5..ff69c62887 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 58e828ef12..38b9ced8cd 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/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift
index 67a8145020..33b0b6cacf 100644
--- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift
@@ -153,7 +153,7 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol {
nslock.unlock()
DispatchQueue.main.async {
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.relayCacheTracker(self, didUpdateCachedRelays: newCachedRelays)
}
}
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
index ea5260b8af..e53c5a50b2 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(
diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
index 762a89b6d1..4b130109bc 100644
--- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
+++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
@@ -152,7 +152,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
)
)
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.storePaymentManager(self, didReceiveEvent: event)
}
} else {
@@ -341,7 +341,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
)
}
- observerList.forEach { observer in
+ observerList.notify { observer in
observer.storePaymentManager(self, didReceiveEvent: .failure(paymentFailure))
}
}
@@ -381,7 +381,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
)
)
- observerList.forEach { observer in
+ observerList.notify { observer in
observer.storePaymentManager(self, didReceiveEvent: event)
}
return
@@ -453,7 +453,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
if let event {
- observerList.forEach { observer in
+ observerList.notify { observer in
observer.storePaymentManager(self, didReceiveEvent: event)
}
}
diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift
index 5b85473e83..088ff0c888 100644
--- a/ios/MullvadVPN/TunnelManager/Tunnel.swift
+++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadTypes
import NetworkExtension
// Switch to stabs on simulator
@@ -187,7 +188,7 @@ final class Tunnel: TunnelProtocol, Equatable {
handleVPNStatus(newStatus)
- observerList.forEach { observer in
+ observerList.notify { observer in
observer.tunnel(self, didReceiveStatus: newStatus)
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index f162810199..13a2d661dd 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -230,7 +230,7 @@ final class TunnelManager: StorePaymentObserver {
let tunnelError = StartTunnelError(underlyingError: error)
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.tunnelManager(self, didFailWithError: tunnelError)
}
}
@@ -266,7 +266,7 @@ final class TunnelManager: StorePaymentObserver {
let tunnelError = StopTunnelError(underlyingError: error)
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.tunnelManager(self, didFailWithError: tunnelError)
}
}
@@ -622,7 +622,7 @@ final class TunnelManager: StorePaymentObserver {
_isConfigurationLoaded = true
DispatchQueue.main.async {
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.tunnelManagerDidLoadConfiguration(self)
}
}
@@ -696,7 +696,7 @@ final class TunnelManager: StorePaymentObserver {
}
DispatchQueue.main.async {
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.tunnelManager(self, didUpdateTunnelStatus: newTunnelStatus)
}
}
@@ -725,7 +725,7 @@ final class TunnelManager: StorePaymentObserver {
if shouldCallDelegate {
DispatchQueue.main.async {
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.tunnelManager(self, didUpdateTunnelSettings: settings)
}
}
@@ -754,7 +754,7 @@ final class TunnelManager: StorePaymentObserver {
if shouldCallDelegate {
DispatchQueue.main.async {
- self.observerList.forEach { observer in
+ self.observerList.notify { observer in
observer.tunnelManager(
self,
didUpdateDeviceState: deviceState,
@@ -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/MullvadVPN/TunnelManager/TunnelStore.swift b/ios/MullvadVPN/TunnelManager/TunnelStore.swift
index b93c33ac45..7c9741a7b9 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelStore.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelStore.swift
@@ -8,6 +8,7 @@
import Foundation
import MullvadLogging
+import MullvadTypes
import NetworkExtension
import UIKit
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
index 68f48c0112..9163613bbe 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/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift
new file mode 100644
index 0000000000..dbd74f0519
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift
@@ -0,0 +1,143 @@
+//
+// ShadowsocksLoaderTests.swift
+// MullvadVPNTests
+//
+// Created by Mojgan on 2024-05-29.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadREST
+@testable import MullvadSettings
+@testable import MullvadTypes
+
+import XCTest
+
+class ShadowsocksLoaderTests: XCTestCase {
+ private let sampleRelays = ServerRelaysResponseStubs.sampleRelays
+
+ private var relayConstraintsUpdater: RelayConstraintsUpdater!
+ private var shadowsocksConfigurationCache: ShadowsocksConfigurationCacheStub!
+ private var relaySelector: ShadowsocksRelaySelectorStub!
+ private var shadowsocksLoader: ShadowsocksLoader!
+ private var relayConstraints = RelayConstraints()
+
+ override func setUpWithError() throws {
+ relayConstraintsUpdater = RelayConstraintsUpdater()
+ shadowsocksConfigurationCache = ShadowsocksConfigurationCacheStub()
+ relaySelector = ShadowsocksRelaySelectorStub(relays: sampleRelays)
+
+ shadowsocksLoader = ShadowsocksLoader(
+ cache: shadowsocksConfigurationCache,
+ relaySelector: relaySelector,
+ constraintsUpdater: relayConstraintsUpdater
+ )
+ }
+
+ func testLoadConfigWithMultihopDisabled() throws {
+ relaySelector.multihopState = .off
+ relaySelector.exitBridgeResult = .success(try XCTUnwrap(closetRelayTo(
+ location: relayConstraints.exitLocations,
+ port: relayConstraints.port,
+ filter: relayConstraints.filter,
+ in: sampleRelays
+ )))
+ relaySelector.entryBridgeResult = .failure(ShadowsocksRelaySelectorStubError())
+
+ let configuration = try XCTUnwrap(shadowsocksLoader.load())
+ XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read()))
+ }
+
+ func testLoadConfigWithMultihopEnabled() throws {
+ relaySelector.multihopState = .on
+ relaySelector.entryBridgeResult = .success(try XCTUnwrap(closetRelayTo(
+ location: relayConstraints.entryLocations,
+ port: relayConstraints.port,
+ filter: relayConstraints.filter,
+ in: sampleRelays
+ )))
+ relaySelector.exitBridgeResult = .failure(ShadowsocksRelaySelectorStubError())
+
+ let configuration = try XCTUnwrap(shadowsocksLoader.load())
+ XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read()))
+ }
+
+ func testConstraintsUpdateClearsCache() throws {
+ relaySelector.exitBridgeResult = .success(try XCTUnwrap(closetRelayTo(
+ location: relayConstraints.exitLocations,
+ port: relayConstraints.port,
+ filter: relayConstraints.filter,
+ in: sampleRelays
+ )))
+ relaySelector.entryBridgeResult = .failure(ShadowsocksRelaySelectorStubError())
+
+ relayConstraints = RelayConstraints(
+ entryLocations: .only(UserSelectedRelays(locations: [.city("ca", "tor")])),
+ exitLocations: .only(UserSelectedRelays(locations: [.country("ae")]))
+ )
+
+ relayConstraintsUpdater.onNewConstraints?(relayConstraints)
+
+ XCTAssertNil(shadowsocksConfigurationCache.cachedConfiguration)
+ }
+
+ private func closetRelayTo(
+ location: RelayConstraint<UserSelectedRelays>,
+ port: RelayConstraint<UInt16>,
+ filter: RelayConstraint<RelayFilter>,
+ in: REST.ServerRelaysResponse
+ ) -> REST.BridgeRelay? {
+ RelaySelector.Shadowsocks.closestRelay(
+ location: location,
+ port: port,
+ filter: filter,
+ in: sampleRelays
+ )
+ }
+}
+
+private class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol {
+ var entryBridgeResult: Result<REST.BridgeRelay, Error> = .failure(ShadowsocksRelaySelectorStubError())
+ var exitBridgeResult: Result<REST.BridgeRelay, Error> = .failure(ShadowsocksRelaySelectorStubError())
+ var multihopState: MultihopState = .off
+
+ private let relays: REST.ServerRelaysResponse
+
+ init(relays: REST.ServerRelaysResponse) {
+ self.relays = relays
+ }
+
+ func selectRelay(with constraints: RelayConstraints) throws -> REST.BridgeRelay? {
+ switch multihopState {
+ case .on:
+ try entryBridgeResult.get()
+ case .off:
+ try exitBridgeResult.get()
+ }
+ }
+
+ func getBridges() throws -> REST.ServerShadowsocks? {
+ RelaySelector.Shadowsocks.tcpBridge(from: relays)
+ }
+}
+
+private class ShadowsocksConfigurationCacheStub: ShadowsocksConfigurationCacheProtocol {
+ private(set) var cachedConfiguration: ShadowsocksConfiguration?
+
+ func read() throws -> ShadowsocksConfiguration {
+ guard let cachedConfiguration else {
+ throw ShadowsocksConfigurationCacheStubError()
+ }
+ return cachedConfiguration
+ }
+
+ func write(_ configuration: ShadowsocksConfiguration) throws {
+ self.cachedConfiguration = configuration
+ }
+
+ func clear() throws {
+ self.cachedConfiguration = nil
+ }
+}
+
+private struct ShadowsocksRelaySelectorStubError: Error {}
+private struct ShadowsocksConfigurationCacheStubError: Error {}
diff --git a/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift b/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift
index ace14e8eb0..22be6900f2 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/MultihopUpdaterTests.swift b/ios/MullvadVPNTests/MullvadSettings/MultihopUpdaterTests.swift
new file mode 100644
index 0000000000..ba6ab3625d
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadSettings/MultihopUpdaterTests.swift
@@ -0,0 +1,46 @@
+//
+// MultihopUpdaterTests.swift
+// MullvadVPNTests
+//
+// Created by Mojgan on 2024-05-29.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadSettings
+import XCTest
+
+class MultihopUpdaterTests: XCTestCase {
+ private var multihopStateListener: MultihopStateListener!
+ private var multihopUpdater: MultihopUpdater!
+ private var observers: [MultihopObserver]!
+
+ override func setUp() {
+ multihopStateListener = MultihopStateListener()
+ multihopUpdater = MultihopUpdater(listener: multihopStateListener)
+ observers = []
+ }
+
+ override func tearDown() {
+ self.observers.forEach {
+ multihopUpdater.removeObserver($0)
+ }
+ }
+
+ func testMultipleListener() {
+ var count = 0
+
+ observers.append(MultihopObserverBlock(didUpdateMultihop: { _, _ in
+ count += 1
+ }))
+
+ observers.append(MultihopObserverBlock(didUpdateMultihop: { _, _ in
+ count += 1
+ }))
+
+ observers.forEach { multihopUpdater.addObserver($0) }
+
+ multihopStateListener.onNewMultihop?(.on)
+
+ XCTAssertEqual(count, 2)
+ }
+}
diff --git a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift
index 89a234f3ce..9ebcee9d40 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"])))
)
@@ -70,4 +70,16 @@ final class TunnelSettingsUpdateTests: XCTestCase {
// Then:
XCTAssertEqual(settings.tunnelQuantumResistance, .on)
}
+
+ func testApplyMultihop() {
+ // Given:
+ var settings = LatestTunnelSettings()
+
+ // When:
+ let update = TunnelSettingsUpdate.multihop(.on)
+ update.apply(to: &settings)
+
+ // Then:
+ XCTAssertEqual(settings.tunnelMultihopState, .on)
+ }
}
diff --git a/ios/MullvadVPNTests/MullvadTypes/MockFileCache.swift b/ios/MullvadVPNTests/MullvadTypes/MockFileCache.swift
index 0b07c788e2..ea13a7356e 100644
--- a/ios/MullvadVPNTests/MullvadTypes/MockFileCache.swift
+++ b/ios/MullvadVPNTests/MullvadTypes/MockFileCache.swift
@@ -45,6 +45,13 @@ final class MockFileCache<Content: Codable & Equatable>: FileCacheProtocol {
state = .exists(content)
}
+ func clear() throws {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+
+ state = .fileNotFound
+ }
+
enum State: Equatable {
/// File does not exist yet.
case fileNotFound
diff --git a/ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift b/ios/MullvadVPNTests/MullvadTypes/RelayConstraintsTests.swift
index 401dc13edd..15ed22663e 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 6b27aa59f8..70c92a8819 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -21,6 +21,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue")
private let providerLogger: Logger
private let constraintsUpdater = RelayConstraintsUpdater()
+ private let multihopStateListener = MultihopStateListener()
+
+ private var multihopUpdater: MultihopUpdater
+ private let settingsReader = SettingsReader()
private var actor: PacketTunnelActor!
private var postQuantumActor: PostQuantumKeyExchangeActor!
@@ -44,6 +48,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
ipOverrideRepository: IPOverrideRepository()
)
+ multihopUpdater = MultihopUpdater(listener: multihopStateListener)
+
super.init()
let transportProvider = setUpTransportProvider(
@@ -67,9 +73,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
)
let accountsProxy = proxyFactory.createAccountsProxy()
let devicesProxy = proxyFactory.createDevicesProxy()
+ let multihopState = (try? settingsReader.read().multihopState) ?? .off
deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy)
- relaySelector = RelaySelectorWrapper(relayCache: ipOverrideWrapper)
+ relaySelector = RelaySelectorWrapper(
+ relayCache: ipOverrideWrapper,
+ multihopUpdater: multihopUpdater,
+ multihopState: multihopState
+ )
+
+ multihopStateListener.onNewMultihop?(multihopState)
actor = PacketTunnelActor(
timings: PacketTunnelActorTimings(),
@@ -78,7 +91,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue),
blockedStateErrorMapper: BlockedStateErrorMapper(),
relaySelector: relaySelector,
- settingsReader: SettingsReader(),
+ settingsReader: settingsReader,
protocolObfuscator: ProtocolObfuscator<UDPOverTCPObfuscator>()
)
@@ -156,12 +169,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let urlSession = REST.makeURLSession()
let urlSessionTransport = URLSessionTransport(urlSession: urlSession)
let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: appContainerURL)
+ let multihopState = (try? settingsReader.read().multihopState) ?? .off
+
+ let shadowsocksRelaySelector = ShadowsocksRelaySelector(
+ relayCache: ipOverrideWrapper,
+ multihopUpdater: multihopUpdater,
+ multihopState: multihopState
+ )
let transportStrategy = TransportStrategy(
datasource: AccessMethodRepository(),
shadowsocksLoader: ShadowsocksLoader(
- shadowsocksCache: shadowsocksCache,
- relayCache: ipOverrideWrapper,
+ cache: shadowsocksCache,
+ relaySelector: shadowsocksRelaySelector,
constraintsUpdater: constraintsUpdater
)
)
diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift
index 73f6fa2674..8254ebb5fd 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift
@@ -8,27 +8,66 @@
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 multihopUpdater: MultihopUpdater
+ private var multihopState: MultihopState = .off
+ private var observer: MultihopObserverBlock!
+
+ deinit {
+ self.multihopUpdater.removeObserver(observer)
+ }
+
+ public init(
+ relayCache: RelayCacheProtocol,
+ multihopUpdater: MultihopUpdater,
+ multihopState: MultihopState
+ ) {
+ self.relayCache = relayCache
+ self.multihopState = multihopState
+ self.multihopUpdater = multihopUpdater
+ self.addObserver()
+ }
+
+ private func addObserver() {
+ self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, multihopState in
+ self?.multihopState = multihopState
+ })
+
+ multihopUpdater.addObserver(observer)
+ }
func selectRelay(
with constraints: RelayConstraints,
connectionAttemptFailureCount: UInt
) throws -> SelectedRelay {
- let selectorResult = try RelaySelector.evaluate(
- relays: relayCache.read().relays,
- constraints: constraints,
- numberOfFailedAttempts: 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
+ )
- 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 ade3b5a3af..2de052e1bd 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/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift
index ffe7cdcc2f..05d60a23f2 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/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
index cdd062b098..37664e6853 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 edb9e99e6d..f5806b9a47 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 9fa8b90258..277854ea8f 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 21f30991bb..e81ae10bf6 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
)
}
}