Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using access methods in transport layer #5648

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions ios/MullvadREST/Transport/AccessMethodIterator.swift
Original file line number Diff line number Diff line change
@@ -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]
}
}
}
33 changes: 33 additions & 0 deletions ios/MullvadREST/Transport/LastReachableApiAccessCache.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
73 changes: 73 additions & 0 deletions ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
119 changes: 18 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,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
}
Expand Down
Loading
Loading