Skip to content

Commit

Permalink
using various api access methods during a network error
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii committed Jan 10, 2024
1 parent 1bcf0ef commit f3ac4c3
Show file tree
Hide file tree
Showing 24 changed files with 641 additions and 438 deletions.
80 changes: 80 additions & 0 deletions ios/MullvadREST/Transport/AccessMethodIterator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// 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 cancellables = Set<Combine.AnyCancellable>()

/// `UserDefaults` key shared by both processes. Used to cache and synchronize last reachable api access method between them.
private let lastReachableConfigurationCacheKey = "LastReachableConfigurationCacheKey"

private var lastReachableApiAccessId: UUID {
lastReachableApiAccessCache.id
}

private var index = 0
private var configurations: [PersistentAccessMethod] = []

init(_ userDefaults: UserDefaults, dataSource: AccessMethodRepositoryDataSource) {
self.dataSource = dataSource
self.lastReachableApiAccessCache = LastReachableApiAccessCache(
key: lastReachableConfigurationCacheKey,
defaultValue: dataSource.directAccess.id,
container: userDefaults
)

self.dataSource
.publisher
.sink { [weak self] newValue in
guard let self else { return }
self.configurations = newValue
self.refreshCacheIfNeeded()
}
.store(in: &cancellables)
}

var current: PersistentAccessMethod {
if enabledConfigurations.isEmpty {
return dataSource.directAccess
} else {
let circularIndex = index % enabledConfigurations.count
return enabledConfigurations[circularIndex]
}
}

private var enabledConfigurations: [PersistentAccessMethod] {
return configurations.filter { $0.isEnabled }
}

private func refreshCacheIfNeeded() {
/// updating the cursor whenever the enabled configurations are updated
guard let idx = self.enabledConfigurations.firstIndex(where: {
$0.id == self.lastReachableApiAccessId
}) else {
self.lastReachableApiAccessCache.id = self.current.id
return
}
self.index = idx
}

/// Picking the next `Enabled` configuration in order they are added
func next() {
if !enabledConfigurations.isEmpty {
index += 1
lastReachableApiAccessCache.id = current.id
} else {
index = 0
}
}
}
32 changes: 32 additions & 0 deletions ios/MullvadREST/Transport/LastReachableApiAccessCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// 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 {
private var appStorage: AppStorage<String>

init(key: String, defaultValue: UUID, container: UserDefaults) {
self.appStorage = AppStorage(
wrappedValue: defaultValue.uuidString,
key: key,
container: container
)
}

var id: UUID {
get {
let value = appStorage.wrappedValue
return UUID(uuidString: value)!
}
set {
appStorage.wrappedValue = newValue.uuidString
}
}
}
79 changes: 79 additions & 0 deletions ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// 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 {
var configuration: ShadowsocksConfiguration { get throws }
func preloadNewConfiguration() throws
}

public class ShadowsocksLoader: ShadowsocksLoaderProtocol {
public var configuration: ShadowsocksConfiguration {
get throws {
try load()
}
}

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 preloadNewConfiguration() throws {
// because the previous shadowsocks configuration was invalid, therefore generate a new one.
let newConfiguration = try create()
try shadowsocksCache.write(newConfiguration)
}

/// Returns the last used shadowsocks configuration, otherwise a new randomized configuration.
private 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
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
)
}
}
111 changes: 16 additions & 95 deletions ios/MullvadREST/Transport/TransportProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand All @@ -61,72 +42,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
}
}

private func socks5() -> RESTTransport? {
return URLSessionSocks5Transport(
urlSession: urlSessionTransport.urlSession,
configuration: Socks5Configuration(address: .ipv4(.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,
Expand All @@ -149,15 +64,21 @@ 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
)
}
return currentTransport
}
Expand Down
Loading

0 comments on commit f3ac4c3

Please sign in to comment.