Skip to content

Commit

Permalink
Using various access methods due a network error
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii committed Jan 11, 2024
1 parent 27ed9a2 commit 3ae100e
Show file tree
Hide file tree
Showing 24 changed files with 656 additions and 443 deletions.
75 changes: 75 additions & 0 deletions ios/MullvadREST/Transport/AccessMethodIterator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// 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 index = 0
private var enabledConfigurations: [PersistentAccessMethod] = []

private var lastReachableApiAccessId: UUID {
lastReachableApiAccessCache.id
}

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] configurations in
guard let self else { return }
self.enabledConfigurations = configurations.filter { $0.isEnabled }
self.refreshCacheIfNeeded()
}
.store(in: &cancellables)
}

var current: 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]
}
}

private func refreshCacheIfNeeded() {
/// Validating the index of `lastReachableApiAccessCache` after any changes in `AccessMethodRepository`
if let idx = enabledConfigurations.firstIndex(where: { $0.id == self.lastReachableApiAccessId }) {
index = idx
} else {
/// When `idx` is `nil`, that means the current configuration is not valid any more
/// Invalidating cache by replacing the `current` to the next enabled access method
lastReachableApiAccessCache.id = current.id
}
}

func next() {
let (partial, isOverflow) = index.addingReportingOverflow(1)
index = isOverflow ? 0 : partial
lastReachableApiAccessCache.id = current.id
}
}
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 {
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 var configuration: ShadowsocksConfiguration {
get throws {
try load()
}
}

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
)
}
}
117 changes: 16 additions & 101 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 @@ -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,
Expand All @@ -153,15 +62,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 3ae100e

Please sign in to comment.