summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@codic.se>2024-01-16 11:24:18 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-01-16 14:20:31 +0100
commitfcc9bf99a12e1a421a1ac9ce2514f876fca5e1ed (patch)
tree737bf21743e397e7f9b45c26294264eb5526261f /ios
parente2bb7328280850a899e2cfc7bdde4a6fd653575f (diff)
downloadmullvadvpn-fcc9bf99a12e1a421a1ac9ce2514f876fca5e1ed.tar.xz
mullvadvpn-fcc9bf99a12e1a421a1ac9ce2514f876fca5e1ed.zip
using different API access methods when it encounters a network error
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadREST/Transport/AccessMethodIterator.swift70
-rw-r--r--ios/MullvadREST/Transport/LastReachableApiAccessCache.swift33
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift73
-rw-r--r--ios/MullvadREST/Transport/TransportProvider.swift119
-rw-r--r--ios/MullvadREST/Transport/TransportStrategy.swift105
-rw-r--r--ios/MullvadRESTTests/AccessMethodRepositoryStub.swift26
-rw-r--r--ios/MullvadRESTTests/ShadowsocksLoaderStub.swift27
-rw-r--r--ios/MullvadRESTTests/TransportStrategyTests.swift237
-rw-r--r--ios/MullvadSettings/AccessMethodRepository.swift41
-rw-r--r--ios/MullvadSettings/AccessMethodRepositoryProtocol.swift5
-rw-r--r--ios/MullvadSettings/AppStorage.swift43
-rw-r--r--ios/MullvadSettings/PersistentAccessMethod.swift9
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj30
-rw-r--r--ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift116
-rw-r--r--ios/MullvadVPN/AppDelegate.swift18
-rw-r--r--ios/MullvadVPN/Classes/AppPreferences.swift43
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift8
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift14
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift85
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift11
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift7
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift4
-rw-r--r--ios/MullvadVPNTests/APIAccessMethodsTests.swift42
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift16
24 files changed, 726 insertions, 456 deletions
diff --git a/ios/MullvadREST/Transport/AccessMethodIterator.swift b/ios/MullvadREST/Transport/AccessMethodIterator.swift
new file mode 100644
index 0000000000..a91c0fd546
--- /dev/null
+++ b/ios/MullvadREST/Transport/AccessMethodIterator.swift
@@ -0,0 +1,70 @@
+//
+// AccessMethodIterator.swift
+// MullvadREST
+//
+// Created by Mojgan on 2024-01-10.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import Foundation
+import MullvadSettings
+
+class AccessMethodIterator {
+ private var lastReachableApiAccessCache: LastReachableApiAccessCache
+ private let dataSource: AccessMethodRepositoryDataSource
+
+ private var index = 0
+ private var enabledConfigurations: [PersistentAccessMethod] = []
+ private var cancellables = Set<Combine.AnyCancellable>()
+
+ private var lastReachableApiAccessId: UUID {
+ lastReachableApiAccessCache.id
+ }
+
+ init(_ userDefaults: UserDefaults, dataSource: AccessMethodRepositoryDataSource) {
+ self.dataSource = dataSource
+ self.lastReachableApiAccessCache = LastReachableApiAccessCache(
+ defaultValue: dataSource.directAccess.id,
+ container: userDefaults
+ )
+
+ self.dataSource
+ .publisher
+ .sink { [weak self] configurations in
+ guard let self else { return }
+ self.enabledConfigurations = configurations.filter { $0.isEnabled }
+ self.refreshCacheIfNeeded()
+ }
+ .store(in: &cancellables)
+ }
+
+ private func refreshCacheIfNeeded() {
+ /// Validating the index of `lastReachableApiAccessCache` after any changes in `AccessMethodRepository`
+ if let firstIndex = enabledConfigurations.firstIndex(where: { $0.id == self.lastReachableApiAccessId }) {
+ index = firstIndex
+ } else {
+ /// When `firstIndex` is `nil`, that means the current configuration is not valid anymore
+ /// Invalidating cache by replacing the `current` to the next enabled access method
+ lastReachableApiAccessCache.id = pick().id
+ }
+ }
+
+ func rotate() {
+ let (partial, isOverflow) = index.addingReportingOverflow(1)
+ index = isOverflow ? 0 : partial
+ lastReachableApiAccessCache.id = pick().id
+ }
+
+ func pick() -> PersistentAccessMethod {
+ if enabledConfigurations.isEmpty {
+ /// Returning `Default` strategy when all is disabled
+ return dataSource.directAccess
+ } else {
+ /// Picking the next `Enabled` configuration in order they are added
+ /// And starting from the beginning when it reaches end
+ let circularIndex = index % enabledConfigurations.count
+ return enabledConfigurations[circularIndex]
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift b/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift
new file mode 100644
index 0000000000..100a591730
--- /dev/null
+++ b/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift
@@ -0,0 +1,33 @@
+//
+// LastReachableApiAccessStorage.swift
+// MullvadREST
+//
+// Created by Mojgan on 2024-01-08.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+struct LastReachableApiAccessCache: Identifiable {
+ /// `UserDefaults` key shared by both processes. Used to cache and synchronize last reachable api access method between them.
+ private let key = "LastReachableConfigurationCacheKey"
+ private var container: UserDefaults
+ private let defaultValue: UUID
+
+ init(defaultValue: UUID, container: UserDefaults) {
+ self.container = container
+ self.defaultValue = defaultValue
+ }
+
+ var id: UUID {
+ get {
+ guard let value = container.string(forKey: key) else {
+ return defaultValue
+ }
+ return UUID(uuidString: value)!
+ }
+ set {
+ container.set(newValue.uuidString, forKey: key)
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
new file mode 100644
index 0000000000..839e3b524f
--- /dev/null
+++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
@@ -0,0 +1,73 @@
+//
+// LocalShadowsocksLoader.swift
+// MullvadREST
+//
+// Created by Mojgan on 2024-01-08.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+public protocol ShadowsocksLoaderProtocol {
+ func load() throws -> ShadowsocksConfiguration
+ func reloadConfiguration() throws
+}
+
+public class ShadowsocksLoader: ShadowsocksLoaderProtocol {
+ private let shadowsocksCache: ShadowsocksConfigurationCache
+ private let relayCache: RelayCacheProtocol
+ private var relayConstraints = RelayConstraints()
+ private let constraintsUpdater: RelayConstraintsUpdater
+
+ public init(
+ shadowsocksCache: ShadowsocksConfigurationCache,
+ relayCache: RelayCacheProtocol,
+ constraintsUpdater: RelayConstraintsUpdater
+ ) {
+ self.shadowsocksCache = shadowsocksCache
+ self.relayCache = relayCache
+ self.constraintsUpdater = constraintsUpdater
+ constraintsUpdater.onNewConstraints = { [weak self] newConstraints in
+ self?.relayConstraints = newConstraints
+ }
+ }
+
+ public func reloadConfiguration() throws {
+ let newConfiguration = try create()
+ try shadowsocksCache.write(newConfiguration)
+ }
+
+ /// 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()
+ } catch {
+ // There is no previous configuration either if this is the first time this code ran
+ // Or because the previous shadowsocks configuration was invalid, therefore generate a new one.
+ let newConfiguration = try create()
+ try shadowsocksCache.write(newConfiguration)
+ return newConfiguration
+ }
+ }
+
+ /// 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
+ )
+
+ guard let bridgeAddress = closestRelay?.ipv4AddrIn, let bridgeConfiguration else { throw POSIXError(.ENOENT) }
+
+ return ShadowsocksConfiguration(
+ address: .ipv4(bridgeAddress),
+ port: bridgeConfiguration.port,
+ password: bridgeConfiguration.password,
+ cipher: bridgeConfiguration.cipher
+ )
+ }
+}
diff --git a/ios/MullvadREST/Transport/TransportProvider.swift b/ios/MullvadREST/Transport/TransportProvider.swift
index e88afd33dc..10d2feafeb 100644
--- a/ios/MullvadREST/Transport/TransportProvider.swift
+++ b/ios/MullvadREST/Transport/TransportProvider.swift
@@ -12,38 +12,19 @@ import MullvadTypes
public final class TransportProvider: RESTTransportProvider {
private let urlSessionTransport: URLSessionTransport
- private let relayCache: RelayCacheProtocol
- private let logger = Logger(label: "TransportProvider")
private let addressCache: REST.AddressCache
- private let shadowsocksCache: ShadowsocksConfigurationCache
private var transportStrategy: TransportStrategy
-
private var currentTransport: RESTTransport?
private let parallelRequestsMutex = NSLock()
- private var relayConstraints = RelayConstraints()
- private let constraintsUpdater: RelayConstraintsUpdater
public init(
urlSessionTransport: URLSessionTransport,
- relayCache: RelayCacheProtocol,
addressCache: REST.AddressCache,
- shadowsocksCache: ShadowsocksConfigurationCache,
- transportStrategy: TransportStrategy,
- constraintsUpdater: RelayConstraintsUpdater
+ transportStrategy: TransportStrategy
) {
self.urlSessionTransport = urlSessionTransport
- self.relayCache = relayCache
self.addressCache = addressCache
- self.shadowsocksCache = shadowsocksCache
self.transportStrategy = transportStrategy
- self.constraintsUpdater = constraintsUpdater
- constraintsUpdater.onNewConstraints = { [weak self] newConstraints in
- self?.parallelRequestsMutex.lock()
- defer {
- self?.parallelRequestsMutex.unlock()
- }
- self?.relayConstraints = newConstraints
- }
}
public func makeTransport() -> RESTTransport? {
@@ -59,78 +40,6 @@ public final class TransportProvider: RESTTransportProvider {
}
}
- // MARK: -
-
- private func shadowsocks() -> RESTTransport? {
- do {
- let shadowsocksConfiguration = try shadowsocksConfiguration()
-
- let shadowsocksURLSession = urlSessionTransport.urlSession
- let shadowsocksTransport = ShadowsocksTransport(
- urlSession: shadowsocksURLSession,
- configuration: shadowsocksConfiguration,
- addressCache: addressCache
- )
- return shadowsocksTransport
- } catch {
- logger.error(error: error, message: "Failed to produce shadowsocks configuration.")
- return nil
- }
- }
-
- // TODO: Pass the socks5 username, password, and ip+port combo here.
- private func socks5() -> RESTTransport? {
- return URLSessionSocks5Transport(
- urlSession: urlSessionTransport.urlSession,
- configuration: Socks5Configuration(proxyEndpoint: AnyIPEndpoint.ipv4(IPv4Endpoint(
- ip: .loopback,
- port: 8889
- ))),
- addressCache: addressCache
- )
- }
-
- /// Returns the last used shadowsocks configuration, otherwise a new randomized configuration.
- private func shadowsocksConfiguration() throws -> ShadowsocksConfiguration {
- // If a previous shadowsocks configuration was in cache, return it directly.
- do {
- return try shadowsocksCache.read()
- } catch {
- // There is no previous configuration either if this is the first time this code ran
- // Or because the previous shadowsocks configuration was invalid, therefore generate a new one.
- return try makeNewShadowsocksConfiguration()
- }
- }
-
- /// Returns a randomly selected shadowsocks configuration.
- private func makeNewShadowsocksConfiguration() throws -> ShadowsocksConfiguration {
- let cachedRelays = try relayCache.read()
- let bridgeConfiguration = RelaySelector.shadowsocksTCPBridge(from: cachedRelays.relays)
- let closestRelay = RelaySelector.closestShadowsocksRelayConstrained(
- by: relayConstraints,
- in: cachedRelays.relays
- )
-
- guard let bridgeAddress = closestRelay?.ipv4AddrIn, let bridgeConfiguration else { throw POSIXError(.ENOENT) }
-
- let newConfiguration = ShadowsocksConfiguration(
- address: .ipv4(bridgeAddress),
- port: bridgeConfiguration.port,
- password: bridgeConfiguration.password,
- cipher: bridgeConfiguration.cipher
- )
-
- do {
- try shadowsocksCache.write(newConfiguration)
- } catch {
- logger.error(error: error, message: "Failed to persist shadowsocks cache.")
- }
-
- return newConfiguration
- }
-
- // MARK: -
-
/// When several requests fail at the same time, prevents the `transportStrategy` from switching multiple times.
///
/// The `strategy` is checked against the `transportStrategy`. When several requests are made and fail in parallel,
@@ -153,15 +62,23 @@ public final class TransportProvider: RESTTransportProvider {
///
/// - Returns: A `RESTTransport` object to make a connection
private func makeTransportInner() -> RESTTransport? {
- if currentTransport == nil {
- switch transportStrategy.connectionTransport() {
- case .useShadowsocks:
- currentTransport = shadowsocks()
- case .useURLSession:
- currentTransport = urlSessionTransport
- case .useSocks5:
- currentTransport = socks5()
- }
+ switch transportStrategy.connectionTransport() {
+ case .direct:
+ currentTransport = urlSessionTransport
+ case let .shadowsocks(configuration):
+ currentTransport = ShadowsocksTransport(
+ urlSession: urlSessionTransport.urlSession,
+ configuration: configuration,
+ addressCache: addressCache
+ )
+ case let .socks5(configuration):
+ currentTransport = URLSessionSocks5Transport(
+ urlSession: urlSessionTransport.urlSession,
+ configuration: configuration,
+ addressCache: addressCache
+ )
+ case .none:
+ currentTransport = nil
}
return currentTransport
}
diff --git a/ios/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift
index 27411d244e..a63891c601 100644
--- a/ios/MullvadREST/Transport/TransportStrategy.swift
+++ b/ios/MullvadREST/Transport/TransportStrategy.swift
@@ -7,56 +7,91 @@
//
import Foundation
+import Logging
+import MullvadSettings
+import MullvadTypes
-public struct TransportStrategy: Equatable {
+public class TransportStrategy: Equatable {
/// The different transports suggested by the strategy
public enum Transport {
- /// Suggests using a direct connection
- case useURLSession
- /// Suggests connecting via Shadowsocks proxy
- case useShadowsocks
- /// Suggests connecting via socks proxy
- case useSocks5
- }
+ /// Connecting a direct connection
+ case direct
+
+ /// Connecting via shadowsocks proxy
+ case shadowsocks(configuration: ShadowsocksConfiguration)
- /// The internal counter for suggested transports.
- ///
- /// A value of `0` means a direct transport suggestion, a value of `1` or `2` means a Shadowsocks transport
- /// suggestion.
- ///
- /// `internal` instead of `private` for testing purposes.
- internal var connectionAttempts: Int
+ /// Connecting via socks proxy
+ case socks5(configuration: Socks5Configuration)
+
+ /// Failing to retrive transport
+ case none
+ }
- /// Enables recording of failed connection attempts.
- private let userDefaults: UserDefaults
+ private let shadowsocksLoader: ShadowsocksLoaderProtocol
- /// `UserDefaults` key shared by both processes. Used to cache and synchronize connection attempts between them.
- internal static let connectionAttemptsSharedCacheKey = "ConnectionAttemptsSharedCacheKey"
+ private let accessMethodIterator: AccessMethodIterator
- public init(_ userDefaults: UserDefaults) {
- self.connectionAttempts = userDefaults.integer(forKey: Self.connectionAttemptsSharedCacheKey)
- self.userDefaults = userDefaults
+ public init(
+ _ userDefaults: UserDefaults,
+ datasource: AccessMethodRepositoryDataSource,
+ shadowsocksLoader: ShadowsocksLoaderProtocol
+ ) {
+ self.shadowsocksLoader = shadowsocksLoader
+ self.accessMethodIterator = AccessMethodIterator(
+ userDefaults,
+ dataSource: datasource
+ )
}
- /// Instructs the strategy that a network connection failed
- ///
- /// Every third failure results in a direct transport suggestion.
- public mutating func didFail() {
- let (partial, isOverflow) = connectionAttempts.addingReportingOverflow(1)
- // (Int.max - 1) is a multiple of 3, go directly to 2 when overflowing
- // to keep the "every third failure" algorithm correct
- connectionAttempts = isOverflow ? 2 : partial
- userDefaults.set(connectionAttempts, forKey: Self.connectionAttemptsSharedCacheKey)
+ /// Rotating between enabled configurations by what order they were added in
+ public func didFail() {
+ let configuration = accessMethodIterator.pick()
+ switch configuration.kind {
+ case .bridges:
+ try? shadowsocksLoader.reloadConfiguration()
+ fallthrough
+ default:
+ self.accessMethodIterator.rotate()
+ }
}
/// The suggested connection transport
- ///
- /// - Returns: `.useURLSession` for every 3rd failed attempt, `.useShadowsocks` otherwise
public func connectionTransport() -> Transport {
- connectionAttempts.isMultiple(of: 3) ? .useURLSession : .useShadowsocks
+ let configuration = accessMethodIterator.pick()
+ switch configuration.proxyConfiguration {
+ case .direct:
+ return .direct
+ case .bridges:
+ do {
+ let configuration = try shadowsocksLoader.load()
+ return .shadowsocks(configuration: configuration)
+ } catch {
+ didFail()
+ guard accessMethodIterator.pick().kind != .bridges else { return .none }
+ return connectionTransport()
+ }
+ case let .shadowsocks(configuration):
+ return .shadowsocks(configuration: ShadowsocksConfiguration(
+ address: configuration.server,
+ port: configuration.port,
+ password: configuration.password,
+ cipher: configuration.cipher.rawValue.description
+ ))
+ case let .socks5(configuration):
+ switch configuration.authentication {
+ case .noAuthentication:
+ return .socks5(configuration: Socks5Configuration(proxyEndpoint: configuration.toAnyIPEndpoint))
+ case let .usernamePassword(username, password):
+ return .socks5(configuration: Socks5Configuration(
+ proxyEndpoint: configuration.toAnyIPEndpoint,
+ username: username,
+ password: password
+ ))
+ }
+ }
}
public static func == (lhs: TransportStrategy, rhs: TransportStrategy) -> Bool {
- lhs.connectionAttempts == rhs.connectionAttempts
+ lhs.accessMethodIterator.pick() == rhs.accessMethodIterator.pick()
}
}
diff --git a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift b/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift
new file mode 100644
index 0000000000..accd7e0f7c
--- /dev/null
+++ b/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift
@@ -0,0 +1,26 @@
+//
+// AccessMethodRepositoryStub.swift
+// MullvadRESTTests
+//
+// Created by Mojgan on 2024-01-02.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import MullvadSettings
+
+typealias PersistentAccessMethod = MullvadSettings.PersistentAccessMethod
+struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource {
+ var directAccess: MullvadSettings.PersistentAccessMethod
+
+ var publisher: AnyPublisher<[MullvadSettings.PersistentAccessMethod], Never> {
+ passthroughSubject.eraseToAnyPublisher()
+ }
+
+ let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([])
+
+ init(accessMethods: [PersistentAccessMethod]) {
+ directAccess = accessMethods.first(where: { $0.kind == .direct })!
+ passthroughSubject.send(accessMethods)
+ }
+}
diff --git a/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift b/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift
new file mode 100644
index 0000000000..4442ddf63f
--- /dev/null
+++ b/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift
@@ -0,0 +1,27 @@
+//
+// ShadowsocksLoaderStub.swift
+// MullvadRESTTests
+//
+// Created by Mojgan on 2024-01-08.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+@testable import MullvadREST
+import MullvadSettings
+import MullvadTypes
+
+struct ShadowsocksLoaderStub: ShadowsocksLoaderProtocol {
+ var configuration: ShadowsocksConfiguration
+ var error: Error?
+
+ func reloadConfiguration() throws {
+ try load()
+ }
+
+ @discardableResult
+ func load() throws -> ShadowsocksConfiguration {
+ if let error { throw error }
+ return configuration
+ }
+}
diff --git a/ios/MullvadRESTTests/TransportStrategyTests.swift b/ios/MullvadRESTTests/TransportStrategyTests.swift
index 420f44f2ff..eee045fec2 100644
--- a/ios/MullvadRESTTests/TransportStrategyTests.swift
+++ b/ios/MullvadRESTTests/TransportStrategyTests.swift
@@ -7,13 +7,19 @@
//
@testable import MullvadREST
+@testable import MullvadSettings
@testable import MullvadTypes
import XCTest
-final class TransportStrategyTests: XCTestCase {
+class TransportStrategyTests: XCTestCase {
var userDefaults: UserDefaults!
static var suiteName: String!
+ private var directAccess: PersistentAccessMethod!
+ private var bridgeAccess: PersistentAccessMethod!
+
+ private var shadowsocksLoader: ShadowsocksLoaderStub!
+
override class func setUp() {
super.setUp()
suiteName = UUID().uuidString
@@ -22,6 +28,27 @@ final class TransportStrategyTests: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
userDefaults = UserDefaults(suiteName: Self.suiteName)
+
+ shadowsocksLoader = ShadowsocksLoaderStub(configuration: ShadowsocksConfiguration(
+ address: .ipv4(.loopback),
+ port: 1080,
+ password: "123",
+ cipher: CipherIdentifiers.CHACHA20.description
+ ))
+
+ directAccess = PersistentAccessMethod(
+ id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .direct
+ )
+
+ bridgeAccess = PersistentAccessMethod(
+ id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .bridges
+ )
}
override func tearDownWithError() throws {
@@ -29,35 +56,205 @@ final class TransportStrategyTests: XCTestCase {
try super.tearDownWithError()
}
- func testEveryThirdConnectionAttemptsIsDirect() {
- loopStrategyTest(with: TransportStrategy(userDefaults), in: 0 ... 12)
+ func testDefaultStrategyIsDirectWhenAllMethodsAreDisabled() throws {
+ directAccess.isEnabled = false
+ bridgeAccess.isEnabled = false
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
+ for _ in 0 ... 4 {
+ transportStrategy.didFail()
+ XCTAssertEqual(transportStrategy.connectionTransport(), .direct)
+ }
}
- func testOverflowingConnectionAttempts() {
- userDefaults.set(Int.max, forKey: TransportStrategy.connectionAttemptsSharedCacheKey)
- let strategy = TransportStrategy(userDefaults)
+ func testReuseSameStrategyWhenEverythingElseIsDisabled() throws {
+ directAccess.isEnabled = false
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
- // (Int.max - 1) is a multiple of 3, so skip the first iteration
- loopStrategyTest(with: strategy, in: 1 ... 12)
+ for _ in 0 ... 10 {
+ transportStrategy.didFail()
+
+ XCTAssertEqual(
+ transportStrategy.connectionTransport(),
+ .shadowsocks(configuration: try XCTUnwrap(shadowsocksLoader.load()))
+ )
+ }
+ }
+
+ func testLoopsFromTheStartAfterTryingAllEnabledStrategies() {
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ PersistentAccessMethod(
+ id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95090")!,
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .shadowsocks(PersistentProxyConfiguration.ShadowsocksConfiguration(
+ server: .ipv4(.loopback),
+ port: 8083,
+ password: "",
+ cipher: .default
+ ))
+ ),
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
+ let accessMethodsCount = 3
+ for i in 0 ..< (accessMethodsCount * 2) {
+ let previousOne = transportStrategy.connectionTransport()
+ transportStrategy.didFail()
+ let currentOne = transportStrategy.connectionTransport()
+ if i % accessMethodsCount == 0 {
+ XCTAssertEqual(previousOne, .direct)
+ } else {
+ XCTAssertNotEqual(previousOne, currentOne)
+ }
+ }
}
- func testConnectionAttemptsAreRecordedAfterFailure() {
- var strategy = TransportStrategy(userDefaults)
+ func testUsesNextWhenItIsNotReachable() {
+ bridgeAccess.isEnabled = false
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ PersistentAccessMethod(
+ id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95090")!,
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .shadowsocks(PersistentProxyConfiguration.ShadowsocksConfiguration(
+ server: .ipv4(.loopback),
+ port: 8083,
+ password: "",
+ cipher: .default
+ ))
+ ),
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
+ XCTAssertEqual(transportStrategy.connectionTransport(), .direct)
+ transportStrategy.didFail()
+ XCTAssertEqual(
+ transportStrategy.connectionTransport(),
+ .shadowsocks(configuration: ShadowsocksConfiguration(
+ address: .ipv4(.loopback),
+ port: 8083,
+ password: "",
+ cipher: ShadowsocksCipherOptions.default.rawValue.description
+ ))
+ )
+ }
- strategy.didFail()
+ func testGoToNextStrategyWhenItFailsToLoadBridgeConfiguration() {
+ shadowsocksLoader.error = IOError.fileNotFound
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
- let recordedValue = userDefaults.integer(forKey: TransportStrategy.connectionAttemptsSharedCacheKey)
- XCTAssertEqual(1, recordedValue)
+ transportStrategy.didFail()
+ XCTAssertEqual(transportStrategy.connectionTransport(), .direct)
}
- private func loopStrategyTest(with strategy: TransportStrategy, in range: ClosedRange<Int>) {
- var strategy = strategy
+ func testNoLoopOnFailureAtLoadingConfigurationWhenBridgeIsOnlyEnabled() {
+ shadowsocksLoader.error = IOError.fileNotFound
+ directAccess.isEnabled = false
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
+ for _ in 0 ... 10 {
+ transportStrategy.didFail()
+ XCTAssertEqual(transportStrategy.connectionTransport(), .none)
+ }
+ }
+
+ func testUsesSocks5WithAuthenticationWhenItReaches() throws {
+ let username = "user"
+ let password = "pass"
+ let authentication = PersistentProxyConfiguration.SocksAuthentication.usernamePassword(
+ username: username,
+ password: password
+ )
+ let socks5Configuration = PersistentProxyConfiguration.SocksConfiguration(
+ server: .ipv4(.loopback),
+ port: 1080,
+ authentication: authentication
+ )
+ let transportStrategy = TransportStrategy(
+ userDefaults,
+ datasource: AccessMethodRepositoryStub(accessMethods: [
+ directAccess,
+ bridgeAccess,
+ PersistentAccessMethod(
+ id: UUID(),
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .socks5(socks5Configuration)
+ ),
+ ]),
+ shadowsocksLoader: shadowsocksLoader
+ )
- for index in range {
- let expectedResult: TransportStrategy.Transport
- expectedResult = index.isMultiple(of: 3) ? .useURLSession : .useShadowsocks
- XCTAssertEqual(strategy.connectionTransport(), expectedResult)
- strategy.didFail()
+ XCTAssertEqual(transportStrategy.connectionTransport(), .direct)
+ transportStrategy.didFail()
+
+ XCTAssertEqual(
+ transportStrategy.connectionTransport(),
+ .shadowsocks(configuration: try XCTUnwrap(shadowsocksLoader.load()))
+ )
+ transportStrategy.didFail()
+
+ guard case let .socks5(configuration) = transportStrategy.connectionTransport(),
+ username == configuration.username,
+ password == configuration.password else {
+ XCTAssertThrowsError("Failed to load Socks5 with authentication")
+ return
}
}
}
+
+extension TransportStrategy.Transport: Equatable {
+ public static func == (lhs: Self, rhs: Self) -> Bool {
+ switch (lhs, rhs) {
+ case(.direct, .direct), (.none, .none):
+ return true
+ case let (.shadowsocks(config1), .shadowsocks(config2)):
+ return config1.port == config2.port && config1.cipher == config2.cipher && config1.password == config2
+ .password
+ case let (.socks5(config1), .socks5(config2)):
+ return config1.proxyEndpoint == config2.proxyEndpoint && config1.username == config2.username && config1
+ .password == config2.password
+ default:
+ return false
+ }
+ }
+}
+
+private enum IOError: Error {
+ case fileNotFound
+}
diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift
index 158501cd7d..6f2b253b2f 100644
--- a/ios/MullvadSettings/AccessMethodRepository.swift
+++ b/ios/MullvadSettings/AccessMethodRepository.swift
@@ -10,33 +10,30 @@ import Combine
import Foundation
public class AccessMethodRepository: AccessMethodRepositoryProtocol {
- let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([
- PersistentAccessMethod(
- id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
- name: "",
- isEnabled: true,
- proxyConfiguration: .direct
- ),
- PersistentAccessMethod(
- id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
- name: "",
- isEnabled: true,
- proxyConfiguration: .bridges
- ),
- ])
+ let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([])
+ private let direct = PersistentAccessMethod(
+ id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .direct
+ )
+ private let bridge = PersistentAccessMethod(
+ id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
+ name: "",
+ isEnabled: true,
+ proxyConfiguration: .bridges
+ )
public var publisher: AnyPublisher<[PersistentAccessMethod], Never> {
passthroughSubject.eraseToAnyPublisher()
}
- public var accessMethods: [PersistentAccessMethod] {
- passthroughSubject.value
+ public var directAccess: PersistentAccessMethod {
+ direct
}
- public static let shared = AccessMethodRepository()
-
- private init() {
- add(passthroughSubject.value)
+ public init() {
+ add([direct, bridge])
}
public func add(_ method: PersistentAccessMethod) {
@@ -96,6 +93,10 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
(try? readApiAccessMethods()) ?? []
}
+ public func reloadWithDefaultsAfterDataRemoval() {
+ add([direct, bridge])
+ }
+
private func readApiAccessMethods() throws -> [PersistentAccessMethod] {
let parser = makeParser()
let data = try SettingsManager.store.read(key: .apiAccessMethods)
diff --git a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift
index 87fbb00c89..f956b37848 100644
--- a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift
+++ b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift
@@ -12,6 +12,8 @@ import Foundation
public protocol AccessMethodRepositoryDataSource {
/// Publisher that propagates a snapshot of persistent store upon modifications.
var publisher: AnyPublisher<[PersistentAccessMethod], Never> { get }
+
+ var directAccess: PersistentAccessMethod { get }
}
public protocol AccessMethodRepositoryProtocol: AccessMethodRepositoryDataSource {
@@ -35,4 +37,7 @@ public protocol AccessMethodRepositoryProtocol: AccessMethodRepositoryDataSource
/// Fetch all access method from the persistent store.
/// - Returns: an array of all persistent access method.
func fetchAll() -> [PersistentAccessMethod]
+
+ /// Refreshes the storage with default values.
+ func reloadWithDefaultsAfterDataRemoval()
}
diff --git a/ios/MullvadSettings/AppStorage.swift b/ios/MullvadSettings/AppStorage.swift
new file mode 100644
index 0000000000..9fd9523ac7
--- /dev/null
+++ b/ios/MullvadSettings/AppStorage.swift
@@ -0,0 +1,43 @@
+//
+// AppStorage.swift
+// MullvadSettings
+//
+// Created by Mojgan on 2024-01-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+@propertyWrapper
+public struct AppStorage<Value> {
+ let key: String
+ let defaultValue: Value
+ let container: UserDefaults
+
+ public var wrappedValue: Value {
+ get {
+ container.value(forKey: key) as? Value ?? defaultValue
+ }
+ set {
+ if let anyOptional = newValue as? AnyOptional,
+ anyOptional.isNil {
+ container.removeObject(forKey: key)
+ } else {
+ container.set(newValue, forKey: key)
+ }
+ }
+ }
+
+ public init(wrappedValue: Value, key: String, container: UserDefaults) {
+ self.defaultValue = wrappedValue
+ self.container = container
+ self.key = key
+ }
+}
+
+protocol AnyOptional {
+ var isNil: Bool { get }
+}
+
+extension Optional: AnyOptional {
+ var isNil: Bool { self == nil }
+}
diff --git a/ios/MullvadSettings/PersistentAccessMethod.swift b/ios/MullvadSettings/PersistentAccessMethod.swift
index 9b4c48a12a..ba61988900 100644
--- a/ios/MullvadSettings/PersistentAccessMethod.swift
+++ b/ios/MullvadSettings/PersistentAccessMethod.swift
@@ -74,6 +74,15 @@ extension PersistentProxyConfiguration {
self.port = port
self.authentication = authentication
}
+
+ public var toAnyIPEndpoint: AnyIPEndpoint {
+ switch server {
+ case let .ipv4(ip):
+ return .ipv4(IPv4Endpoint(ip: ip, port: port))
+ case let .ipv6(ip):
+ return .ipv6(IPv6Endpoint(ip: ip, port: port))
+ }
+ }
}
/// Shadowsocks configuration.
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index dde9de3805..63eadb0dfa 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -730,6 +730,12 @@
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.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 */; };
+ F0164EC12B4C03980020268D /* LastReachableApiAccessCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */; };
+ F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; };
+ F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; };
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; };
F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; };
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
@@ -1784,6 +1790,12 @@
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
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>"; };
+ 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>"; };
+ F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastReachableApiAccessCache.swift; sourceTree = "<group>"; };
+ F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderStub.swift; sourceTree = "<group>"; };
+ F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodIterator.swift; sourceTree = "<group>"; };
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = "<group>"; };
F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededViewController.swift; sourceTree = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
@@ -2780,6 +2792,7 @@
588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */,
5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */,
58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */,
+ F0164EBB2B482E430020268D /* AppStorage.swift */,
A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */,
580F8B8528197958002E0998 /* DNSSettings.swift */,
06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */,
@@ -2807,8 +2820,8 @@
isa = PBXGroup;
children = (
58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */,
- 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */,
58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */,
+ 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */,
);
path = Mocks;
sourceTree = "<group>";
@@ -3218,10 +3231,12 @@
58FBFBE7291622580020E046 /* MullvadRESTTests */ = {
isa = PBXGroup;
children = (
+ F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */,
58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */,
- A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */,
- 58B4656F2A98C53300467203 /* RequestExecutorTests.swift */,
58BDEB9E2A98F6B400F578F2 /* Mocks */,
+ 58B4656F2A98C53300467203 /* RequestExecutorTests.swift */,
+ F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */,
+ A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */,
);
path = MullvadRESTTests;
sourceTree = "<group>";
@@ -3414,7 +3429,9 @@
F0DC77A02B2223290087F09D /* Transport */ = {
isa = PBXGroup;
children = (
+ F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */,
F0DC77A32B2315800087F09D /* Direct */,
+ F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */,
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */,
58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */,
F0DC77A22B2314EF0087F09D /* Shadowsocks */,
@@ -3441,6 +3458,7 @@
F04F95A02B21D24400431E08 /* shadowsocks.h */,
F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */,
F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */,
+ F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */,
F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */,
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */,
);
@@ -4347,6 +4365,7 @@
06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */,
A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */,
A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */,
+ F0164EC12B4C03980020268D /* LastReachableApiAccessCache.swift in Sources */,
06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */,
06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */,
A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */,
@@ -4356,6 +4375,7 @@
F0DDE4162B220458006B57A7 /* TransportProvider.swift in Sources */,
06799AEF28F98E4800ACD94E /* RetryStrategy.swift in Sources */,
06799AE128F98E4800ACD94E /* SSLPinningURLSessionDelegate.swift in Sources */,
+ F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */,
A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */,
A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */,
A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */,
@@ -4387,6 +4407,7 @@
589E76C02A9378F100E502F3 /* RESTRequestExecutor.swift in Sources */,
A90763C12B2858320045ADF0 /* URLSessionSocks5Transport.swift in Sources */,
06799AE528F98E4800ACD94E /* HTTP.swift in Sources */,
+ F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */,
A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */,
A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */,
06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */,
@@ -4580,6 +4601,7 @@
58B2FDE02AA71D5C003EB5C6 /* TunnelSettings.swift in Sources */,
A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */,
58B2FDE42AA71D5C003EB5C6 /* SettingsManager.swift in Sources */,
+ F0164EBC2B482E430020268D /* AppStorage.swift in Sources */,
58B2FDE62AA71D5C003EB5C6 /* DeviceState.swift in Sources */,
58FE25BF2AA72311003D1918 /* MigrationManager.swift in Sources */,
58B2FDEF2AA720C4003EB5C6 /* ApplicationTarget.swift in Sources */,
@@ -5126,8 +5148,10 @@
buildActionMask = 2147483647;
files = (
58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */,
+ F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */,
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */,
58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */,
+ F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */,
58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */,
58BDEB9B2A98F58600F578F2 /* TimeServerProxy.swift in Sources */,
58BDEB992A98F4ED00F578F2 /* AnyTransport.swift in Sources */,
diff --git a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift
deleted file mode 100644
index 90f4bf875a..0000000000
--- a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift
+++ /dev/null
@@ -1,116 +0,0 @@
-//
-// AccessMethodRepository.swift
-// MullvadVPN
-//
-// Created by Jon Petersson on 12/12/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import Combine
-import Foundation
-import MullvadSettings
-
-class AccessMethodRepository: AccessMethodRepositoryProtocol {
- let publisher: PassthroughSubject<[PersistentAccessMethod], Never> = .init()
-
- static let shared = AccessMethodRepository()
-
- private var defaultDirectMethod: PersistentAccessMethod {
- PersistentAccessMethod(
- id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
- name: "",
- isEnabled: true,
- proxyConfiguration: .direct
- )
- }
-
- private var defaultBridgesMethod: PersistentAccessMethod {
- PersistentAccessMethod(
- id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
- name: "",
- isEnabled: true,
- proxyConfiguration: .bridges
- )
- }
-
- init() {
- add([defaultDirectMethod, defaultBridgesMethod])
- }
-
- func add(_ method: PersistentAccessMethod) {
- add([method])
- }
-
- func add(_ methods: [PersistentAccessMethod]) {
- var storedMethods = fetchAll()
-
- methods.forEach { method in
- guard !storedMethods.contains(where: { $0.id == method.id }) else { return }
- storedMethods.append(method)
- }
-
- do {
- try writeApiAccessMethods(storedMethods)
- } catch {
- print("Could not add access method(s): \(methods) \nError: \(error)")
- }
- }
-
- func update(_ method: PersistentAccessMethod) {
- var methods = fetchAll()
-
- guard let index = methods.firstIndex(where: { $0.id == method.id }) else { return }
- methods[index] = method
-
- do {
- try writeApiAccessMethods(methods)
- } catch {
- print("Could not update access method: \(method) \nError: \(error)")
- }
- }
-
- func delete(id: UUID) {
- var methods = fetchAll()
- guard let index = methods.firstIndex(where: { $0.id == id }) else { return }
-
- // Prevent removing methods that have static UUIDs and are always present.
- let method = methods[index]
- if !method.kind.isPermanent {
- methods.remove(at: index)
- }
-
- do {
- try writeApiAccessMethods(methods)
- } catch {
- print("Could not delete access method with id: \(id) \nError: \(error)")
- }
- }
-
- func fetch(by id: UUID) -> PersistentAccessMethod? {
- fetchAll().first { $0.id == id }
- }
-
- func fetchAll() -> [PersistentAccessMethod] {
- (try? readApiAccessMethods()) ?? []
- }
-
- private func readApiAccessMethods() throws -> [PersistentAccessMethod] {
- let parser = makeParser()
- let data = try SettingsManager.store.read(key: .apiAccessMethods)
-
- return try parser.parseUnversionedPayload(as: [PersistentAccessMethod].self, from: data)
- }
-
- private func writeApiAccessMethods(_ accessMethods: [PersistentAccessMethod]) throws {
- let parser = makeParser()
- let data = try parser.produceUnversionedPayload(accessMethods)
-
- try SettingsManager.store.write(data, for: .apiAccessMethods)
-
- publisher.send(accessMethods)
- }
-
- private func makeParser() -> SettingsParser {
- SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
- }
-}
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index fe254248c2..8a49c19fef 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -42,6 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private var relayConstraintsObserver: TunnelBlockObserver!
private let migrationManager = MigrationManager()
+ private(set) var accessMethodRepository = AccessMethodRepository()
+
// MARK: - Application lifecycle
func application(
@@ -92,15 +94,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// This init cannot fail as long as the security group identifier is valid
let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!
- let transportStrategy = TransportStrategy(sharedUserDefaults)
+ let transportStrategy = TransportStrategy(
+ sharedUserDefaults,
+ datasource: accessMethodRepository,
+ shadowsocksLoader: ShadowsocksLoader(
+ shadowsocksCache: shadowsocksCache,
+ relayCache: relayCache,
+ constraintsUpdater: constraintsUpdater
+ )
+ )
let transportProvider = TransportProvider(
urlSessionTransport: urlSessionTransport,
- relayCache: relayCache,
addressCache: addressCache,
- shadowsocksCache: shadowsocksCache,
- transportStrategy: transportStrategy,
- constraintsUpdater: constraintsUpdater
+ transportStrategy: transportStrategy
)
setUpTransportMonitor(transportProvider: transportProvider)
setUpSimulatorHost(transportProvider: transportProvider)
@@ -483,6 +490,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
SettingsManager.resetStore(completely: true)
+ self.accessMethodRepository.reloadWithDefaultsAfterDataRemoval()
}
FirstTimeLaunch.setHasFinished()
diff --git a/ios/MullvadVPN/Classes/AppPreferences.swift b/ios/MullvadVPN/Classes/AppPreferences.swift
index 95e49ad2a0..292a42bddb 100644
--- a/ios/MullvadVPN/Classes/AppPreferences.swift
+++ b/ios/MullvadVPN/Classes/AppPreferences.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadSettings
protocol AppPreferencesDataSource {
var isShownOnboarding: Bool { get set }
@@ -18,49 +19,13 @@ enum AppStorageKey: String {
case isShownOnboarding, isAgreedToTermsOfService, lastSeenChangeLogVersion
}
-@propertyWrapper
-struct AppStorage<Value> {
- let key: AppStorageKey
- let defaultValue: Value
- let container: UserDefaults
-
- var wrappedValue: Value {
- get {
- let value = container.value(forKey: key.rawValue)
- return value.flatMap { $0 as? Value } ?? defaultValue
- }
- set {
- if let anyOptional = newValue as? AnyOptional,
- anyOptional.isNil {
- container.removeObject(forKey: key.rawValue)
- } else {
- container.set(newValue, forKey: key.rawValue)
- }
- }
- }
-
- init(wrappedValue: Value, _ key: AppStorageKey, container: UserDefaults = .standard) {
- self.defaultValue = wrappedValue
- self.container = container
- self.key = key
- }
-}
-
final class AppPreferences: AppPreferencesDataSource {
- @AppStorage(.isShownOnboarding)
+ @AppStorage(key: AppStorageKey.isShownOnboarding.rawValue, container: .standard)
var isShownOnboarding = true
- @AppStorage(.isAgreedToTermsOfService)
+ @AppStorage(key: AppStorageKey.isAgreedToTermsOfService.rawValue, container: .standard)
var isAgreedToTermsOfService = false
- @AppStorage(.lastSeenChangeLogVersion)
+ @AppStorage(key: AppStorageKey.lastSeenChangeLogVersion.rawValue, container: .standard)
var lastSeenChangeLogVersion = ""
}
-
-protocol AnyOptional {
- var isNil: Bool { get }
-}
-
-extension Optional: AnyOptional {
- var isNil: Bool { self == nil }
-}
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index 077214fc39..c56c376721 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -77,6 +77,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
private var tunnelObserver: TunnelObserver?
private var appPreferences: AppPreferencesDataSource
private var outgoingConnectionService: OutgoingConnectionServiceHandling
+ private var accessMethodRepository: AccessMethodRepositoryProtocol
private var outOfTimeTimer: Timer?
@@ -92,7 +93,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
devicesProxy: DeviceHandling,
accountsProxy: RESTAccountHandling,
outgoingConnectionService: OutgoingConnectionServiceHandling,
- appPreferences: AppPreferencesDataSource
+ appPreferences: AppPreferencesDataSource,
+ accessMethodRepository: AccessMethodRepositoryProtocol
) {
self.tunnelManager = tunnelManager
self.storePaymentManager = storePaymentManager
@@ -102,6 +104,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
self.accountsProxy = accountsProxy
self.appPreferences = appPreferences
self.outgoingConnectionService = outgoingConnectionService
+ self.accessMethodRepository = accessMethodRepository
super.init()
@@ -757,7 +760,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
let navigationController = CustomNavigationController()
let coordinator = SettingsCoordinator(
navigationController: navigationController,
- interactorFactory: interactorFactory
+ interactorFactory: interactorFactory,
+ accessMethodRepository: accessMethodRepository
)
coordinator.didFinish = { [weak self] _ in
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift
index afd687847f..c355343d2c 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift
@@ -12,20 +12,24 @@ import UIKit
class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordinator {
let navigationController: UINavigationController
- let accessMethodRepo: AccessMethodRepository = .shared
+ let accessMethodRepository: AccessMethodRepositoryProtocol
let proxyConfigurationTester: ProxyConfigurationTester = .shared
var presentationContext: UIViewController {
navigationController
}
- init(navigationController: UINavigationController) {
+ init(
+ navigationController: UINavigationController,
+ accessMethodRepository: AccessMethodRepositoryProtocol
+ ) {
self.navigationController = navigationController
+ self.accessMethodRepository = accessMethodRepository
}
func start(animated: Bool) {
let listController = ListAccessMethodViewController(
- interactor: ListAccessMethodInteractor(repo: accessMethodRepo)
+ interactor: ListAccessMethodInteractor(repo: accessMethodRepository)
)
listController.delegate = self
navigationController.pushViewController(listController, animated: animated)
@@ -34,7 +38,7 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin
private func addNew() {
let coordinator = AddAccessMethodCoordinator(
navigationController: CustomNavigationController(),
- accessMethodRepo: accessMethodRepo,
+ accessMethodRepo: accessMethodRepository,
proxyConfigurationTester: proxyConfigurationTester
)
@@ -48,7 +52,7 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin
let editCoordinator = EditAccessMethodCoordinator(
navigationController: navigationController,
- accessMethodRepo: accessMethodRepo,
+ accessMethodRepo: accessMethodRepository,
proxyConfigurationTester: proxyConfigurationTester,
methodIdentifier: item.id
)
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift
deleted file mode 100644
index 88137dd148..0000000000
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// AccessMethodKind.swift
-// MullvadVPN
-//
-// Created by pronebird on 02/11/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadSettings
-
-/// A kind of API access method.
-enum AccessMethodKind: Equatable, Hashable, CaseIterable {
- /// Direct communication.
- case direct
-
- /// Communication over bridges.
- case bridges
-
- /// Communication over shadowsocks.
- case shadowsocks
-
- /// Communication over socks v5 proxy.
- case socks5
-}
-
-extension AccessMethodKind {
- /// Returns `true` if the method is permanent and cannot be deleted.
- var isPermanent: Bool {
- switch self {
- case .direct, .bridges:
- true
- case .shadowsocks, .socks5:
- false
- }
- }
-
- /// Returns all access method kinds that can be added by user.
- static var allUserDefinedKinds: [AccessMethodKind] {
- allCases.filter { !$0.isPermanent }
- }
-}
-
-extension PersistentAccessMethod {
- /// A kind of access method.
- var kind: AccessMethodKind {
- switch proxyConfiguration {
- case .direct:
- .direct
- case .bridges:
- .bridges
- case .shadowsocks:
- .shadowsocks
- case .socks5:
- .socks5
- }
- }
-}
-
-extension AccessMethodKind {
- /// Returns localized description describing the access method.
- var localizedDescription: String {
- switch self {
- case .direct:
- NSLocalizedString("DIRECT", tableName: "APIAccess", value: "Direct", comment: "")
- case .bridges:
- NSLocalizedString("BRIDGES", tableName: "APIAccess", value: "Bridges", comment: "")
- case .shadowsocks:
- NSLocalizedString("SHADOWSOCKS", tableName: "APIAccess", value: "Shadowsocks", comment: "")
- case .socks5:
- NSLocalizedString("SOCKS_V5", tableName: "APIAccess", value: "Socks5", comment: "")
- }
- }
-
- /// Returns `true` if access method is configurable.
- /// Methods that aren't configurable do not offer any additional configuration.
- var hasProxyConfiguration: Bool {
- switch self {
- case .direct, .bridges:
- false
- case .shadowsocks, .socks5:
- true
- }
- }
-}
diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift
index 4d9abb81f8..a0023dd237 100644
--- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift
@@ -7,6 +7,7 @@
//
import MullvadLogging
+import MullvadSettings
import Operations
import Routing
import UIKit
@@ -37,6 +38,7 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
private let interactorFactory: SettingsInteractorFactory
private var currentRoute: SettingsNavigationRoute?
private var modalRoute: SettingsNavigationRoute?
+ private let accessMethodRepository: AccessMethodRepositoryProtocol
let navigationController: UINavigationController
@@ -60,10 +62,12 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
/// - interactorFactory: an instance of a factory that produces interactors for the child routes.
init(
navigationController: UINavigationController,
- interactorFactory: SettingsInteractorFactory
+ interactorFactory: SettingsInteractorFactory,
+ accessMethodRepository: AccessMethodRepositoryProtocol
) {
self.navigationController = navigationController
self.interactorFactory = interactorFactory
+ self.accessMethodRepository = accessMethodRepository
}
/// Start the coordinator fllow.
@@ -246,7 +250,10 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
))
case .apiAccess:
- return .childCoordinator(ListAccessMethodCoordinator(navigationController: navigationController))
+ return .childCoordinator(ListAccessMethodCoordinator(
+ navigationController: navigationController,
+ accessMethodRepository: accessMethodRepository
+ ))
case .faq:
// Handled separately and presented as a modal.
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index 0719559d70..cb7347c9e6 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -31,6 +31,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand
UIApplication.shared.delegate as! AppDelegate
}
+ private var accessMethodRepository: AccessMethodRepositoryProtocol {
+ appDelegate.accessMethodRepository
+ }
+
private var tunnelManager: TunnelManager {
appDelegate.tunnelManager
}
@@ -71,7 +75,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand
outgoingConnectionService: OutgoingConnectionService(
outgoingConnectionProxy: OutgoingConnectionProxy(urlSession: URLSession(configuration: .ephemeral))
),
- appPreferences: AppPreferences()
+ appPreferences: AppPreferences(),
+ accessMethodRepository: accessMethodRepository
)
appCoordinator?.onShowSettings = { [weak self] in
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift
index 58b5201cf6..c3f07dc72c 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift
@@ -19,7 +19,9 @@ final class SettingsInteractor {
tunnelManager.deviceState
}
- init(tunnelManager: TunnelManager) {
+ init(
+ tunnelManager: TunnelManager
+ ) {
self.tunnelManager = tunnelManager
let tunnelObserver =
diff --git a/ios/MullvadVPNTests/APIAccessMethodsTests.swift b/ios/MullvadVPNTests/APIAccessMethodsTests.swift
index 2163c900c6..f30886f5e3 100644
--- a/ios/MullvadVPNTests/APIAccessMethodsTests.swift
+++ b/ios/MullvadVPNTests/APIAccessMethodsTests.swift
@@ -21,14 +21,15 @@ final class APIAccessMethodsTests: XCTestCase {
}
override func tearDownWithError() throws {
- let repository = AccessMethodRepository.shared
+ let repository = AccessMethodRepository()
repository.fetchAll().forEach {
repository.delete(id: $0.id)
}
}
func testDefaultAccessMethodsExist() throws {
- let storedMethods = AccessMethodRepository.shared.fetchAll()
+ let repository = AccessMethodRepository()
+ let storedMethods = repository.fetchAll()
let hasDirectMethod = storedMethods.contains { method in
method.kind == .direct
@@ -43,60 +44,69 @@ final class APIAccessMethodsTests: XCTestCase {
}
func testAddingSocks5AccessMethod() throws {
+ let repository = AccessMethodRepository()
+
let uuid = UUID()
let methodToStore = socks5AccessMethod(with: uuid)
+ repository.add(methodToStore)
- AccessMethodRepository.shared.add(methodToStore)
- let storedMethod = AccessMethodRepository.shared.fetch(by: uuid)
+ let storedMethod = repository.fetch(by: uuid)
XCTAssertEqual(methodToStore.id, storedMethod?.id)
}
func testAddingShadowSocksAccessMethod() throws {
+ let repository = AccessMethodRepository()
+
let uuid = UUID()
let methodToStore = shadowsocksAccessMethod(with: uuid)
+ repository.add(methodToStore)
- AccessMethodRepository.shared.add(methodToStore)
- let storedMethod = AccessMethodRepository.shared.fetch(by: uuid)
+ let storedMethod = repository.fetch(by: uuid)
XCTAssertEqual(methodToStore.id, storedMethod?.id)
}
func testAddingDuplicateAccessMethodDoesNothing() throws {
+ let repository = AccessMethodRepository()
+
let methodToStore = socks5AccessMethod(with: UUID())
- AccessMethodRepository.shared.add(methodToStore)
- AccessMethodRepository.shared.add(methodToStore)
- let storedMethods = AccessMethodRepository.shared.fetchAll()
+ repository.add(methodToStore)
+ repository.add(methodToStore)
+
+ let storedMethods = repository.fetchAll()
// Account for .direct and .bridges that are always added by default.
XCTAssertEqual(storedMethods.count, 3)
}
func testUpdatingAccessMethod() throws {
+ let repository = AccessMethodRepository()
+
let uuid = UUID()
var methodToStore = socks5AccessMethod(with: uuid)
-
- AccessMethodRepository.shared.add(methodToStore)
+ repository.add(methodToStore)
let newName = "Renamed method"
methodToStore.name = newName
- AccessMethodRepository.shared.update(methodToStore)
+ repository.update(methodToStore)
- let storedMethod = AccessMethodRepository.shared.fetch(by: uuid)
+ let storedMethod = repository.fetch(by: uuid)
XCTAssertEqual(storedMethod?.name, newName)
}
func testDeletingAccessMethod() throws {
+ let repository = AccessMethodRepository()
let uuid = UUID()
let methodToStore = socks5AccessMethod(with: uuid)
- AccessMethodRepository.shared.add(methodToStore)
- AccessMethodRepository.shared.delete(id: uuid)
+ repository.add(methodToStore)
+ repository.delete(id: uuid)
- let storedMethod = AccessMethodRepository.shared.fetch(by: uuid)
+ let storedMethod = repository.fetch(by: uuid)
XCTAssertNil(storedMethod)
}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
index cd3ccc8bd1..735ee455b9 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -9,6 +9,7 @@
import Foundation
import MullvadLogging
import MullvadREST
+import MullvadSettings
import MullvadTypes
import NetworkExtension
import PacketTunnelCore
@@ -41,15 +42,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// This init cannot fail as long as the security group identifier is valid
let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!
- let transportStrategy = TransportStrategy(sharedUserDefaults)
+ let transportStrategy = TransportStrategy(
+ sharedUserDefaults,
+ datasource: AccessMethodRepository(),
+ shadowsocksLoader: ShadowsocksLoader(
+ shadowsocksCache: shadowsocksCache,
+ relayCache: relayCache,
+ constraintsUpdater: constraintsUpdater
+ )
+ )
let transportProvider = TransportProvider(
urlSessionTransport: urlSessionTransport,
- relayCache: relayCache,
addressCache: addressCache,
- shadowsocksCache: shadowsocksCache,
- transportStrategy: transportStrategy,
- constraintsUpdater: constraintsUpdater
+ transportStrategy: transportStrategy
)
super.init()