Skip to content

Commit

Permalink
Cache the transportStrategy in the UserDefaults
Browse files Browse the repository at this point in the history
  • Loading branch information
buggmagnet committed Oct 12, 2023
1 parent 7f092c3 commit 1c524d5
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 37 deletions.
25 changes: 19 additions & 6 deletions ios/MullvadREST/RESTTransportStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

public struct TransportStrategy: Codable, Equatable {
public struct TransportStrategy: Equatable {
/// The different transports suggested by the strategy
public enum Transport {
/// Suggests using a direct connection
Expand All @@ -23,19 +23,28 @@ public struct TransportStrategy: Codable, Equatable {
/// suggestion.
///
/// `internal` instead of `private` for testing purposes.
internal var connectionAttempts: UInt
internal var connectionAttempts: Int

public init() {
connectionAttempts = 0
/// Enables recording of failed connection attempts.
private let userDefaults: UserDefaults

/// `UserDefaults` key shared by both processes. Used to cache and synchronize connection attempts between them.
internal static let connectionAttemptsSharedCacheKey = "ConnectionAttemptsSharedCacheKey"

public init(_ userDefaults: UserDefaults) {
self.connectionAttempts = userDefaults.integer(forKey: Self.connectionAttemptsSharedCacheKey)
self.userDefaults = userDefaults
}

/// 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)
// UInt.max is a multiple of 3, go directly to 1 when overflowing
connectionAttempts = isOverflow ? 1 : partial
// (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)
}

/// The suggested connection transport
Expand All @@ -44,4 +53,8 @@ public struct TransportStrategy: Codable, Equatable {
public func connectionTransport() -> Transport {
connectionAttempts.isMultiple(of: 3) ? .useURLSession : .useShadowsocks
}

public static func == (lhs: TransportStrategy, rhs: TransportStrategy) -> Bool {
lhs.connectionAttempts == rhs.connectionAttempts
}
}
48 changes: 30 additions & 18 deletions ios/MullvadRESTTests/TransportStrategyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,53 @@
//

@testable import MullvadREST
@testable import MullvadTypes
import XCTest

final class TransportStrategyTests: XCTestCase {
var userDefaults: UserDefaults!
static var suiteName: String!

override class func setUp() {
super.setUp()
suiteName = UUID().uuidString
}

override func setUpWithError() throws {
try super.setUpWithError()
userDefaults = UserDefaults(suiteName: Self.suiteName)
}

override func tearDownWithError() throws {
userDefaults.removePersistentDomain(forName: Self.suiteName)
try super.tearDownWithError()
}

func testEveryThirdConnectionAttemptsIsDirect() {
loopStrategyTest(with: TransportStrategy())
loopStrategyTest(with: TransportStrategy(userDefaults), in: 0 ... 12)
}

func testOverflowingConnectionAttempts() {
var strategy = TransportStrategy()
strategy.connectionAttempts = UInt.max
userDefaults.set(Int.max, forKey: TransportStrategy.connectionAttemptsSharedCacheKey)
let strategy = TransportStrategy(userDefaults)

loopStrategyTest(with: strategy)
// (Int.max - 1) is a multiple of 3, so skip the first iteration
loopStrategyTest(with: strategy, in: 1 ... 12)
}

func testLoadingFromCacheDoesNotImpactStrategy() throws {
var strategy = TransportStrategy()
func testConnectionAttemptsAreRecordedAfterFailure() {
var strategy = TransportStrategy(userDefaults)

// Fail twice, the next suggested transport mode should be via Shadowsocks proxy
strategy.didFail()
strategy.didFail()
XCTAssertEqual(strategy.connectionTransport(), .useShadowsocks)

// Serialize the strategy and reload it from memory to simulate an application restart
let encodedRawStrategy = try JSONEncoder().encode(strategy)
var reloadedStrategy = try JSONDecoder().decode(TransportStrategy.self, from: encodedRawStrategy)

// This should be the third failure, the next suggested transport will be a direct one
reloadedStrategy.didFail()
XCTAssertEqual(reloadedStrategy.connectionTransport(), .useURLSession)
let recordedValue = userDefaults.integer(forKey: TransportStrategy.connectionAttemptsSharedCacheKey)
XCTAssertEqual(1, recordedValue)
}

private func loopStrategyTest(with strategy: TransportStrategy) {
private func loopStrategyTest(with strategy: TransportStrategy, in range: ClosedRange<Int>) {
var strategy = strategy

for index in 0 ... 12 {
for index in range {
let expectedResult: TransportStrategy.Transport
expectedResult = index.isMultiple(of: 3) ? .useURLSession : .useShadowsocks
XCTAssertEqual(strategy.connectionTransport(), expectedResult)
Expand Down
2 changes: 1 addition & 1 deletion ios/MullvadTransport/TransportProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public final class TransportProvider: RESTTransportProvider {
relayCache: RelayCache,
addressCache: REST.AddressCache,
shadowsocksCache: ShadowsocksConfigurationCache,
transportStrategy: TransportStrategy = .init(),
transportStrategy: TransportStrategy,
constraintsUpdater: RelayConstraintsUpdater
) {
self.urlSessionTransport = urlSessionTransport
Expand Down
29 changes: 17 additions & 12 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,6 @@
7AF6E5F02A95051E00F2679D /* RouterBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */; };
7AF6E5F12A95F4A500F2679D /* DurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FBFBF0291630700020E046 /* DurationTests.swift */; };
7AF9BE992A4E0FE900DBFEDB /* MarkdownStylingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */; };
A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */; };
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; };
A93D13782A1F60A6001EB0B1 /* shadowsocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 586F2BE129F6916F009E6924 /* shadowsocks.h */; settings = {ATTRIBUTES = (Private, ); }; };
A9467E7F2A29DEFE000DC21F /* RelayCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */; };
Expand All @@ -505,6 +504,8 @@
A97F1F472A1F4E1A00ECEFDE /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; };
A97F1F482A1F4E1A00ECEFDE /* MullvadTransport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; };
A9A1DE792AD5708E0073F689 /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; };
A9A1DE7A2AD5709A0073F689 /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; };
A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A8A8EA2A262AB30086D569 /* FileCache.swift */; };
A9AD31D72A6AB68B00141BE8 /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; };
A9B2CF722A1F64CD0013CC6C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
Expand Down Expand Up @@ -1460,7 +1461,6 @@
7AE47E512A17972A000418DA /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = "<group>"; };
7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownStylingOptions.swift; sourceTree = "<group>"; };
A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; };
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = "<group>"; };
A92ECC232A7802520052F1B1 /* StoredAccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredAccountData.swift; sourceTree = "<group>"; };
Expand All @@ -1472,6 +1472,7 @@
A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadTransport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadTransport.h; sourceTree = "<group>"; };
A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; };
A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; };
A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; };
A9D96B192A8247C100A5C673 /* MigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1744,11 +1745,12 @@
06799ABD28F98E1D00ACD94E /* MullvadREST */ = {
isa = PBXGroup;
children = (
582FFA82290A84E700895745 /* Info.plist */,
062B45A228FD4C0F00746E77 /* Assets */,
06799ABE28F98E1D00ACD94E /* MullvadREST.h */,
06AC114128F8413A0037AF9A /* AddressCache.swift */,
062B45A228FD4C0F00746E77 /* Assets */,
5897F1732913EAF800AF5695 /* ExponentialBackoff.swift */,
06FAE67128F83CA40033DD93 /* HTTP.swift */,
582FFA82290A84E700895745 /* Info.plist */,
06799ABE28F98E1D00ACD94E /* MullvadREST.h */,
06FAE67B28F83CA50033DD93 /* REST.swift */,
06FAE67228F83CA40033DD93 /* RESTAccessTokenManager.swift */,
06FAE66828F83CA30033DD93 /* RESTAccountsProxy.swift */,
Expand All @@ -1761,20 +1763,19 @@
06FAE66928F83CA30033DD93 /* RESTError.swift */,
06FAE66F28F83CA40033DD93 /* RESTNetworkOperation.swift */,
06FAE66E28F83CA40033DD93 /* RESTProxy.swift */,
589E76BF2A9378F100E502F3 /* RESTRequestExecutor.swift */,
06FAE66728F83CA30033DD93 /* RESTProxyFactory.swift */,
589E76BF2A9378F100E502F3 /* RESTRequestExecutor.swift */,
06FAE66A28F83CA30033DD93 /* RESTRequestFactory.swift */,
06FAE67428F83CA40033DD93 /* RESTRequestHandler.swift */,
06FAE66628F83CA30033DD93 /* RESTResponseHandler.swift */,
06FAE67628F83CA40033DD93 /* RESTRetryStrategy.swift */,
5897F1732913EAF800AF5695 /* ExponentialBackoff.swift */,
06FAE67528F83CA40033DD93 /* RESTTaskIdentifier.swift */,
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */,
A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */,
58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */,
A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */,
06FAE66528F83CA30033DD93 /* RESTURLSession.swift */,
06FAE67728F83CA40033DD93 /* ServerRelaysResponse.swift */,
06FAE66B28F83CA30033DD93 /* SSLPinningURLSessionDelegate.swift */,
58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */,
);
path = MullvadREST;
sourceTree = "<group>";
Expand Down Expand Up @@ -3808,6 +3809,7 @@
06799ADE28F98E4800ACD94E /* RESTRequestHandler.swift in Sources */,
06799AEF28F98E4800ACD94E /* RESTRetryStrategy.swift in Sources */,
06799AE128F98E4800ACD94E /* SSLPinningURLSessionDelegate.swift in Sources */,
A9A1DE792AD5708E0073F689 /* RESTTransportStrategy.swift in Sources */,
06799AEA28F98E4800ACD94E /* RESTProxy.swift in Sources */,
06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */,
06799ADB28F98E4800ACD94E /* RESTProxyFactory.swift in Sources */,
Expand All @@ -3824,7 +3826,6 @@
06799ADF28F98E4800ACD94E /* RESTDevicesProxy.swift in Sources */,
06799ADA28F98E4800ACD94E /* RESTResponseHandler.swift in Sources */,
062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */,
A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */,
06799AE428F98E4800ACD94E /* RESTAccountsProxy.swift in Sources */,
5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */,
06799AE328F98E4800ACD94E /* RESTNetworkOperation.swift in Sources */,
Expand Down Expand Up @@ -4338,6 +4339,7 @@
buildActionMask = 2147483647;
files = (
58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */,
A9A1DE7A2AD5709A0073F689 /* RESTTransportStrategy.swift in Sources */,
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */,
58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */,
58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */,
Expand Down Expand Up @@ -5706,11 +5708,14 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = CKG9MXH72F;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F;
"DEVELOPMENT_TEAM[sdk=macosx*]" = CKG9MXH72F;
GENERATE_INFOPLIST_FILE = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.MullvadRESTTests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
6 changes: 6 additions & 0 deletions ios/MullvadVPN/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD

let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession())
let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL)

// This init cannot fail as long as the security group identifier is valid
let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!
let transportStrategy = TransportStrategy(sharedUserDefaults)

let transportProvider = TransportProvider(
urlSessionTransport: urlSessionTransport,
relayCache: relayCache,
addressCache: addressCache,
shadowsocksCache: shadowsocksCache,
transportStrategy: transportStrategy,
constraintsUpdater: constraintsUpdater
)
setUpTransportMonitor(transportProvider: transportProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let urlSession = REST.makeURLSession()
let urlSessionTransport = URLSessionTransport(urlSession: urlSession)
let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL)

// This init cannot fail as long as the security group identifier is valid
let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!
let transportStrategy = TransportStrategy(sharedUserDefaults)

let transportProvider = TransportProvider(
urlSessionTransport: urlSessionTransport,
relayCache: relayCache,
addressCache: addressCache,
shadowsocksCache: shadowsocksCache,
transportStrategy: transportStrategy,
constraintsUpdater: constraintsUpdater
)

Expand Down

0 comments on commit 1c524d5

Please sign in to comment.