diff --git a/ios/MullvadREST/RESTAPIProxy.swift b/ios/MullvadREST/RESTAPIProxy.swift index b547d92e45d6..70522f8b03e0 100644 --- a/ios/MullvadREST/RESTAPIProxy.swift +++ b/ios/MullvadREST/RESTAPIProxy.swift @@ -11,8 +11,46 @@ import MullvadTypes import struct WireGuardKitTypes.IPAddressRange import class WireGuardKitTypes.PublicKey +public protocol APIQuerying { + func getAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable + + func getRelays( + etag: String?, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable + + func createApplePayment( + accountNumber: String, + receiptString: Data + ) -> any RESTRequestExecutor + + func createApplePayment( + accountNumber: String, + receiptString: Data, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable + + func sendProblemReport( + _ body: REST.ProblemReportRequest, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable + + func submitVoucher( + voucherCode: String, + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable +} + extension REST { - public final class APIProxy: Proxy { + public final class APIProxy: Proxy, APIQuerying { public init(configuration: AuthProxyConfiguration) { super.init( name: "APIProxy", @@ -27,7 +65,7 @@ extension REST { public func getAddressList( retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<[AnyIPEndpoint]> + completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in try self.requestFactory.createRequest( @@ -54,7 +92,7 @@ extension REST { public func getRelays( etag: String?, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in var requestBuilder = try self.requestFactory.createRequestBuilder( @@ -170,7 +208,7 @@ extension REST { accountNumber: String, receiptString: Data, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { return createApplePayment(accountNumber: accountNumber, receiptString: receiptString).execute( retryStrategy: retryStrategy, @@ -181,7 +219,7 @@ extension REST { public func sendProblemReport( _ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in var requestBuilder = try self.requestFactory.createRequestBuilder( @@ -222,7 +260,7 @@ extension REST { voucherCode: String, accountNumber: String, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in diff --git a/ios/MullvadREST/RESTAccessTokenManager.swift b/ios/MullvadREST/RESTAccessTokenManager.swift index 17c812020d34..d15bd6b9cb1e 100644 --- a/ios/MullvadREST/RESTAccessTokenManager.swift +++ b/ios/MullvadREST/RESTAccessTokenManager.swift @@ -11,8 +11,17 @@ import MullvadLogging import MullvadTypes import Operations +public protocol RESTAccessTokenManagement { + func getAccessToken( + accountNumber: String, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable + + func invalidateAllTokens() +} + extension REST { - public final class AccessTokenManager { + public final class AccessTokenManager: RESTAccessTokenManagement { private let logger = Logger(label: "REST.AccessTokenManager") private let operationQueue = AsyncOperationQueue.makeSerial() private let dispatchQueue = DispatchQueue(label: "REST.AccessTokenManager.dispatchQueue") @@ -23,9 +32,9 @@ extension REST { proxy = authenticationProxy } - func getAccessToken( + public func getAccessToken( accountNumber: String, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { let operation = ResultBlockOperation(dispatchQueue: dispatchQueue) { finish -> Cancellable in diff --git a/ios/MullvadREST/RESTAccountsProxy.swift b/ios/MullvadREST/RESTAccountsProxy.swift index 29e41e027e15..fad967aac703 100644 --- a/ios/MullvadREST/RESTAccountsProxy.swift +++ b/ios/MullvadREST/RESTAccountsProxy.swift @@ -9,8 +9,29 @@ import Foundation import MullvadTypes +public protocol RESTAccountHandling { + func createAccount( + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable + + func getAccountData(accountNumber: String) -> any RESTRequestExecutor + + func getAccountData( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable + + func deleteAccount( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable +} + extension REST { - public final class AccountsProxy: Proxy { + public final class AccountsProxy: Proxy, RESTAccountHandling { public init(configuration: AuthProxyConfiguration) { super.init( name: "AccountsProxy", @@ -25,7 +46,7 @@ extension REST { public func createAccount( retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in try self.requestFactory.createRequest( @@ -81,7 +102,7 @@ extension REST { public func getAccountData( accountNumber: String, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { return getAccountData(accountNumber: accountNumber).execute( retryStrategy: retryStrategy, @@ -92,7 +113,7 @@ extension REST { public func deleteAccount( accountNumber: String, retryStrategy: RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler(createURLRequest: { endpoint, authorization in var requestBuilder = try self.requestFactory.createRequestBuilder( diff --git a/ios/MullvadREST/RESTAuthenticationProxy.swift b/ios/MullvadREST/RESTAuthenticationProxy.swift index fb899aa2c146..0b5cabe8cb18 100644 --- a/ios/MullvadREST/RESTAuthenticationProxy.swift +++ b/ios/MullvadREST/RESTAuthenticationProxy.swift @@ -23,10 +23,10 @@ extension REST { ) } - func getAccessToken( + public func getAccessToken( accountNumber: String, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in var requestBuilder = try self.requestFactory.createRequestBuilder( @@ -57,7 +57,7 @@ extension REST { } } - struct AccessTokenData: Decodable { + public struct AccessTokenData: Decodable { let accessToken: String let expiry: Date } diff --git a/ios/MullvadREST/RESTDevicesProxy.swift b/ios/MullvadREST/RESTDevicesProxy.swift index f273fac7d5b7..6ec8e299e5a3 100644 --- a/ios/MullvadREST/RESTDevicesProxy.swift +++ b/ios/MullvadREST/RESTDevicesProxy.swift @@ -11,8 +11,45 @@ import MullvadTypes import struct WireGuardKitTypes.IPAddressRange import class WireGuardKitTypes.PublicKey +public protocol DeviceHandling { + func getDevice( + accountNumber: String, + identifier: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable + + func getDevices( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<[Device]> + ) -> Cancellable + + func createDevice( + accountNumber: String, + request: REST.CreateDeviceRequest, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable + + func deleteDevice( + accountNumber: String, + identifier: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable + + func rotateDeviceKey( + accountNumber: String, + identifier: String, + publicKey: PublicKey, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable +} + extension REST { - public final class DevicesProxy: Proxy { + public final class DevicesProxy: Proxy, DeviceHandling { public init(configuration: AuthProxyConfiguration) { super.init( name: "DevicesProxy", @@ -31,7 +68,7 @@ extension REST { accountNumber: String, identifier: String, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -87,7 +124,7 @@ extension REST { public func getDevices( accountNumber: String, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler<[Device]> + completion: @escaping ProxyCompletionHandler<[Device]> ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -125,7 +162,7 @@ extension REST { accountNumber: String, request: CreateDeviceRequest, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -164,7 +201,7 @@ extension REST { accountNumber: String, identifier: String, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -226,7 +263,7 @@ extension REST { identifier: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, - completion: @escaping CompletionHandler + completion: @escaping ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in diff --git a/ios/MullvadREST/RESTProxy.swift b/ios/MullvadREST/RESTProxy.swift index 4c6d138fd51b..6ca64d85ee46 100644 --- a/ios/MullvadREST/RESTProxy.swift +++ b/ios/MullvadREST/RESTProxy.swift @@ -10,10 +10,10 @@ import Foundation import MullvadTypes import Operations +public typealias ProxyCompletionHandler = (Result) -> Void + extension REST { public class Proxy { - public typealias CompletionHandler = (Result) -> Void - /// Synchronization queue used by network operations. let dispatchQueue: DispatchQueue @@ -49,7 +49,7 @@ extension REST { retryStrategy: REST.RetryStrategy, requestHandler: RESTRequestHandler, responseHandler: some RESTResponseHandler, - completionHandler: @escaping CompletionHandler + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { let executor = makeRequestExecutor( name: name, @@ -89,7 +89,7 @@ extension REST { /// Creates new network operation but does not schedule it for execution. func makeOperation( retryStrategy: REST.RetryStrategy, - completionHandler: ((Result) -> Void)? = nil + completionHandler: ProxyCompletionHandler? = nil ) -> NetworkOperation { return NetworkOperation( name: getTaskIdentifier(name: name), @@ -110,7 +110,7 @@ extension REST { func execute( retryStrategy: REST.RetryStrategy, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping ProxyCompletionHandler ) -> Cancellable { let operation = operationFactory.makeOperation( retryStrategy: retryStrategy, @@ -137,7 +137,7 @@ extension REST { } } - func execute(completionHandler: @escaping (Result) -> Void) -> Cancellable { + func execute(completionHandler: @escaping ProxyCompletionHandler) -> Cancellable { return execute(retryStrategy: .noRetry, completionHandler: completionHandler) } diff --git a/ios/MullvadREST/RESTProxyFactory.swift b/ios/MullvadREST/RESTProxyFactory.swift index 2436cbf3e87d..43201890cc02 100644 --- a/ios/MullvadREST/RESTProxyFactory.swift +++ b/ios/MullvadREST/RESTProxyFactory.swift @@ -40,15 +40,15 @@ extension REST { self.configuration = configuration } - public func createAPIProxy() -> REST.APIProxy { + public func createAPIProxy() -> APIQuerying { REST.APIProxy(configuration: configuration) } - public func createAccountsProxy() -> REST.AccountsProxy { + public func createAccountsProxy() -> RESTAccountHandling { REST.AccountsProxy(configuration: configuration) } - public func createDevicesProxy() -> REST.DevicesProxy { + public func createDevicesProxy() -> DeviceHandling { REST.DevicesProxy(configuration: configuration) } } diff --git a/ios/MullvadTransport/TransportProvider.swift b/ios/MullvadTransport/TransportProvider.swift index 7eb6ff4632bf..88c3e1fc8a23 100644 --- a/ios/MullvadTransport/TransportProvider.swift +++ b/ios/MullvadTransport/TransportProvider.swift @@ -15,7 +15,7 @@ import RelaySelector public final class TransportProvider: RESTTransportProvider { private let urlSessionTransport: URLSessionTransport - private let relayCache: RelayCache + private let relayCache: RelayCacheProtocol private let logger = Logger(label: "TransportProvider") private let addressCache: REST.AddressCache private let shadowsocksCache: ShadowsocksConfigurationCache @@ -28,7 +28,7 @@ public final class TransportProvider: RESTTransportProvider { public init( urlSessionTransport: URLSessionTransport, - relayCache: RelayCache, + relayCache: RelayCacheProtocol, addressCache: REST.AddressCache, shadowsocksCache: ShadowsocksConfigurationCache, transportStrategy: TransportStrategy, diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 7986b4245d7d..3f6ab488ff17 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -48,12 +48,8 @@ 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; }; 5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; - 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; }; - 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; 580810E52A30E13A00B74552 /* DeviceStateAccessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */; }; - 580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */; }; 580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; }; - 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; }; 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; }; 58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */; }; 580D6B8A2AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */; }; @@ -63,7 +59,6 @@ 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; }; 58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; }; 58153071294CBE8B00D1702E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; - 58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */; }; 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */; }; 581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2722A1E227D0046ED47 /* RESTTypes.swift */; }; 581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; }; @@ -82,7 +77,6 @@ 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; }; 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; - 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */; }; 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; }; 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; }; 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */; }; @@ -90,11 +84,11 @@ 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; 5838321B2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */; }; 5838321D2AC1C54600EA2071 /* TaskSleepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */; }; - 5838321F2AC3160A00EA2071 /* Actor+KeyPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */; }; - 583832212AC3174700EA2071 /* Actor+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */; }; - 583832232AC3181400EA2071 /* Actor+ErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832222AC3181400EA2071 /* Actor+ErrorState.swift */; }; - 583832252AC318A100EA2071 /* Actor+ConnectionMonitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */; }; - 583832272AC3193600EA2071 /* Actor+SleepCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */; }; + 5838321F2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */; }; + 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */; }; + 583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */; }; + 583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */; }; + 583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */; }; 583832292AC3DF1300EA2071 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* Command.swift */; }; 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */; }; 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; @@ -119,7 +113,6 @@ 585A02EB2A4B285800C6CAFF /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; }; 585A02ED2A4B28F300C6CAFF /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */; }; 585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */; }; - 585B1FF22AB0BC69008AD470 /* State+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */; }; 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; }; 585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; }; 585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; }; @@ -147,7 +140,7 @@ 586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; 586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; }; 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14572AC463BB00245C01 /* CommandChannelTests.swift */; }; - 586C145A2AC4735F00245C01 /* Actor+Public.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14592AC4735F00245C01 /* Actor+Public.swift */; }; + 586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; 586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */; }; 5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871167E2910035700D41AAC /* PreferencesInteractor.swift */; }; @@ -189,9 +182,7 @@ 588E4EAE28FEEDD8008046E3 /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; }; 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; }; - 58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */; }; 58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */; }; - 58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; }; 58915D6E2A26037A0066445B /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58915D6D2A26037A0066445B /* WireGuardKitTypes */; }; 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; }; @@ -199,7 +190,6 @@ 5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5893716928817A45004EE76C /* DeviceManagementViewController.swift */; }; 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58968FAD28743E2000B799DC /* TunnelInteractor.swift */; }; 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; - 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; }; 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */; }; 5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5897F1732913EAF800AF5695 /* ExponentialBackoff.swift */; }; 5898D29F29017DD000EB5EBA /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; @@ -223,7 +213,6 @@ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; 58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */; }; 58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */; }; - 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */; }; 58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */; }; 58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */; }; @@ -250,7 +239,6 @@ 58B2FDEF2AA720C4003EB5C6 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */; }; 58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B4656F2A98C53300467203 /* RequestExecutorTests.swift */; }; - 58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; }; 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; }; 58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB142489139B00095626 /* RESTError+Display.swift */; }; @@ -262,8 +250,6 @@ 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; }; 58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4F82964B08300D72515 /* MapViewController.swift */; }; - 58C3FA662A38549D006A450A /* MockFileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3FA652A38549D006A450A /* MockFileCache.swift */; }; - 58C3FA682A385C89006A450A /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3FA672A385C89006A450A /* FileCacheTests.swift */; }; 58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 58C76A092A33850E00100D75 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 58C76A0B2A338E4300100D75 /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */; }; @@ -293,10 +279,6 @@ 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */; }; 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; }; 58C9B8CE2ABB252E00040B46 /* DeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */; }; - 58C9B8D02ABB254000040B46 /* DeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */; }; - 58C9B8D12ABB255100040B46 /* DeviceCheckOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */; }; - 58C9B8D22ABB255100040B46 /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; - 58C9B8D32ABB255100040B46 /* DeviceCheckRemoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */; }; 58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */; }; 58CAFA032985367600BE19F7 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAFA01298530DC00BE19F7 /* Promise.swift */; }; 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; @@ -400,7 +382,6 @@ 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; }; 58F3F36A2AA08E3C00D3B0A4 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */; }; 58F7753D2AB8473200425B47 /* BlockedStateErrorMapperStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */; }; - 58F775432AB9E3EF00425B47 /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */; }; 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; }; 58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */; }; 58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheTrackerObserver.swift */; }; @@ -414,11 +395,11 @@ 58FE25C22AA72729003D1918 /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; 58FE25C62AA72779003D1918 /* PacketTunnelCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C7A4362A863F440060C66F /* PacketTunnelCore.framework */; }; 58FE25CE2AA72802003D1918 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; - 58FE25D42AA729B5003D1918 /* ActorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25D32AA729B5003D1918 /* ActorTests.swift */; }; + 58FE25D42AA729B5003D1918 /* PacketTunnelActorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */; }; 58FE25D72AA72A8F003D1918 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824030C2A811B0000163DE8 /* State.swift */; }; 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */; }; 58FE25D92AA72A8F003D1918 /* AutoCancellingTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */; }; - 58FE25DA2AA72A8F003D1918 /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */; }; + 58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.swift */; }; 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ED3A132A7C199C0085CE65 /* StartOptions.swift */; }; 58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */; }; 58FE25DF2AA72A9B003D1918 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; }; @@ -428,7 +409,7 @@ 58FE25EE2AA7764E003D1918 /* TunnelAdapterDummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */; }; 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */; }; 58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */; }; - 58FE25F42AA9D730003D1918 /* Actor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */; }; + 58FE25F42AA9D730003D1918 /* PacketTunnelActor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */; }; 58FE65952AB1D90600E53CB5 /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; }; 58FF23A32AB09BEE003A2AF2 /* DeviceChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */; }; @@ -446,6 +427,10 @@ 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; }; 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; 7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; }; + 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; }; + 7A3FD1B62AD542110042BEA6 /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */; }; + 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; }; + 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A42DECD2A09064C00B209BE /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */; }; 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; }; @@ -485,15 +470,22 @@ 7ABCA5B42A9349F20044A708 /* Routing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7ABCA5B72A9353C60044A708 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; }; 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */; }; + 7AD0AA1C2AD6A63F00119E10 /* PacketTunnelActorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */; }; + 7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */; }; + 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */; }; + 7AD0AA212AD6CB0000119E10 /* URLRequestProxyStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */; }; 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AE47E522A17972A000418DA /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE47E512A17972A000418DA /* AlertViewController.swift */; }; + 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */; }; 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 */; }; + A900E9B82ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */; }; + A900E9BA2ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */; }; + A900E9BC2ACC609200C95F67 /* DevicesProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */; }; + A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */; }; + A900E9C02ACC661900C95F67 /* AccessTokenManager+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.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 */; }; - A9467E802A29E0A6000DC21F /* AddressCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */; }; A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; }; A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; }; A95F86B72A1F53BA00245DAC /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */; }; @@ -502,19 +494,124 @@ 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 */; }; + A988DF212ADD293D00D807EF /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; }; + A988DF242ADD307200D807EF /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; }; A9A1DE792AD5708E0073F689 /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; }; - A9A1DE7A2AD5709A0073F689 /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; }; + A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; }; + A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */; }; + A9A5F9E32ACB05160083449F /* AccountDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */; }; + A9A5F9E42ACB05160083449F /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; }; + A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; + A9A5F9E62ACB05160083449F /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; }; + A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; }; + A9A5F9E82ACB05160083449F /* MarkdownStylingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */; }; + A9A5F9E92ACB05160083449F /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; + A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; + A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */; }; + A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; + A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; }; + A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; }; + A9A5F9EF2ACB05160083449F /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; + A9A5F9F02ACB05160083449F /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; + A9A5F9F12ACB05160083449F /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; + A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; }; + A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */; }; + A9A5F9F42ACB05160083449F /* AccountExpiryInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */; }; + A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; + A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; }; + A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */; }; + A9A5F9F82ACB05160083449F /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; + A9A5F9F92ACB05160083449F /* NotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E252943522400D5980C /* NotificationProvider.swift */; }; + A9A5F9FA2ACB05160083449F /* InAppNotificationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */; }; + A9A5F9FB2ACB05160083449F /* InAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */; }; + A9A5F9FC2ACB05160083449F /* SystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E272943527300D5980C /* SystemNotificationProvider.swift */; }; + A9A5F9FD2ACB05160083449F /* NotificationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5877F94D2A0A59AA0052D9E9 /* NotificationResponse.swift */; }; + A9A5F9FE2ACB05160083449F /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B7535266528A200DEF7E9 /* NotificationManager.swift */; }; + A9A5F9FF2ACB05160083449F /* NotificationManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */; }; + A9A5FA002ACB05160083449F /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; + A9A5FA012ACB05160083449F /* RelayCacheTrackerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheTrackerObserver.swift */; }; + A9A5FA022ACB05160083449F /* RelayCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */; }; + A9A5FA032ACB05160083449F /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; + A9A5FA042ACB05160083449F /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; + A9A5FA052ACB05160083449F /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; + A9A5FA062ACB05160083449F /* SimulatorTunnelProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */; }; + A9A5FA072ACB05160083449F /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; }; + A9A5FA082ACB05160083449F /* StorePaymentBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27629093A4F0096FC88 /* StorePaymentBlockObserver.swift */; }; + A9A5FA092ACB05160083449F /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; }; + A9A5FA0A2ACB05160083449F /* StorePaymentEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27429093A310096FC88 /* StorePaymentEvent.swift */; }; + A9A5FA0B2ACB05160083449F /* StorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */; }; + A9A5FA0C2ACB05160083449F /* StorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */; }; + A9A5FA0D2ACB05160083449F /* StorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */; }; + A9A5FA0E2ACB05160083449F /* StorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */; }; + A9A5FA0F2ACB05160083449F /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; }; + A9A5FA102ACB05160083449F /* PacketTunnelTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */; }; + A9A5FA112ACB05160083449F /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; }; + A9A5FA122ACB05160083449F /* DeleteAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */; }; + A9A5FA132ACB05160083449F /* LoadTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */; }; + A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; }; + A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; }; + A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */; }; + A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; + A9A5FA182ACB05160083449F /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; }; + A9A5FA192ACB05160083449F /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; }; + A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */; }; + A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; }; + A9A5FA1C2ACB05160083449F /* Tunnel+Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */; }; + A9A5FA1D2ACB05160083449F /* TunnelBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27229091D6D0096FC88 /* TunnelBlockObserver.swift */; }; + A9A5FA1E2ACB05160083449F /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; }; + A9A5FA1F2ACB05160083449F /* TunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58968FAD28743E2000B799DC /* TunnelInteractor.swift */; }; + A9A5FA202ACB05160083449F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; + A9A5FA212ACB05160083449F /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; }; + A9A5FA222ACB05160083449F /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; }; + A9A5FA232ACB05160083449F /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; }; + A9A5FA242ACB05160083449F /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; }; + A9A5FA252ACB05160083449F /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; }; + A9A5FA262ACB05160083449F /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; }; + A9A5FA272ACB05160083449F /* VPNConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */; }; + A9A5FA282ACB05160083449F /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; }; + A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */; }; + A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */; }; + A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; }; + A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */; }; + A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FBFBF0291630700020E046 /* DurationTests.swift */; }; + A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3FA672A385C89006A450A /* FileCacheTests.swift */; }; + A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */; }; + A9A5FA302ACB05160083449F /* InputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */; }; + A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3FA652A38549D006A450A /* MockFileCache.swift */; }; + A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */; }; + A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; + A9A5FA342ACB05160083449F /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; }; + A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */; }; + A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */; }; + A9A5FA372ACB052D0083449F /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; + A9A5FA382ACB05600083449F /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; }; + A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA0152242560B004F3011 /* UIColor+Palette.swift */; }; + A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */; }; + A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; }; + A9A5FA3C2ACB05B20083449F /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; }; + A9A5FA3D2ACB05D90083449F /* DeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */; }; + A9A5FA3E2ACB05D90083449F /* DeviceCheckOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */; }; + A9A5FA3F2ACB05D90083449F /* DeviceCheckRemoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */; }; + A9A5FA402ACB05D90083449F /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; }; + A9A5FA412ACB05D90083449F /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; + A9A5FA422ACB05D90083449F /* DeviceStateAccessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */; }; + A9A5FA432ACB05F20083449F /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.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 */; }; + A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; }; + A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */; }; + A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */; }; A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; }; A9D99BA02A1F7F3A00DE27D3 /* TransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */; }; A9D99BA52A1F808900DE27D3 /* RelayCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 063F02732902B63F001FA09F /* RelayCache.framework */; }; A9D99BA62A1F809C00DE27D3 /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; }; A9D99BA92A1F81B700DE27D3 /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; }; + A9E031782ACB09930095D843 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */; }; + A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */; }; + A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */; }; + A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; }; A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */; }; A9EC20E62A5C488D0040D56E /* Haversine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20E52A5C488D0040D56E /* Haversine.swift */; }; - A9EC20E82A5D3A8C0040D56E /* CoordinatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */; }; A9EC20F02A5D79ED0040D56E /* TunnelObfuscation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A9EC20F42A5D96030040D56E /* Midpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20F32A5D96030040D56E /* Midpoint.swift */; }; E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; }; @@ -525,7 +622,6 @@ F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; }; F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; }; F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; }; - F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */; }; F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; }; F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.swift */; }; @@ -609,13 +705,6 @@ remoteGlobalIDString = 5840231E2A406BF5007B27AC; remoteInfo = TunnelObfuscation; }; - 586A0DCD2A20E359006C731C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 58CE5E58224146200008646E /* Project object */; - proxyType = 1; - remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; - remoteInfo = MullvadTypes; - }; 586A0DD62A20E4A9006C731C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -686,13 +775,6 @@ remoteGlobalIDString = 06799ABB28F98E1D00ACD94E; remoteInfo = MullvadREST; }; - 58D2239F294C89B50029F5F8 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 58CE5E58224146200008646E /* Project object */; - proxyType = 1; - remoteGlobalIDString = 06799ABB28F98E1D00ACD94E; - remoteInfo = MullvadREST; - }; 58D223C1294C8AE90029F5F8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -868,6 +950,13 @@ remoteGlobalIDString = A97F1F402A1F4E1A00ECEFDE; remoteInfo = MullvadTransport; }; + A988DF222ADD305300D807EF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5898D29729017DAC00EB5EBA; + remoteInfo = RelaySelector; + }; A9B2CF702A1F64B20013CC6C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -1123,11 +1212,11 @@ 5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = ""; }; 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Mocks.swift"; sourceTree = ""; }; 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskSleepTests.swift; sourceTree = ""; }; - 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+KeyPolicy.swift"; sourceTree = ""; }; - 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+NetworkReachability.swift"; sourceTree = ""; }; - 583832222AC3181400EA2071 /* Actor+ErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+ErrorState.swift"; sourceTree = ""; }; - 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+ConnectionMonitoring.swift"; sourceTree = ""; }; - 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+SleepCycle.swift"; sourceTree = ""; }; + 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+KeyPolicy.swift"; sourceTree = ""; }; + 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+NetworkReachability.swift"; sourceTree = ""; }; + 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+ErrorState.swift"; sourceTree = ""; }; + 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+ConnectionMonitoring.swift"; sourceTree = ""; }; + 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+SleepCycle.swift"; sourceTree = ""; }; 583832282AC3DF1300EA2071 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannel.swift; sourceTree = ""; }; 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStateAccessor.swift; sourceTree = ""; }; @@ -1163,7 +1252,6 @@ 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPUnsafeListener.swift; sourceTree = ""; }; 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = ""; }; 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = ""; }; - 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "State+Extensions.swift"; sourceTree = ""; }; 585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = ""; }; 585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = ""; }; 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = ""; }; @@ -1190,7 +1278,7 @@ 586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = ""; }; 586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = ""; }; 586C14572AC463BB00245C01 /* CommandChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannelTests.swift; sourceTree = ""; }; - 586C14592AC4735F00245C01 /* Actor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Public.swift"; sourceTree = ""; }; + 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Public.swift"; sourceTree = ""; }; 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = ""; }; 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReaderProtocol.swift; sourceTree = ""; }; 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Duration.swift"; sourceTree = ""; }; @@ -1361,7 +1449,7 @@ 58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportProvider.swift; sourceTree = ""; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = ""; }; 58E9C3832A4EF15300CFDEAC /* WireGuardAdapter+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapter+Async.swift"; sourceTree = ""; }; - 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = ""; }; + 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActor.swift; sourceTree = ""; }; 58EC06792A8D208D00BEB973 /* TunnelDeviceInfoStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoStub.swift; sourceTree = ""; }; 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCounters.swift; sourceTree = ""; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = ""; }; @@ -1381,7 +1469,6 @@ 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellingTask.swift; sourceTree = ""; }; 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedStateErrorMapperStub.swift; sourceTree = ""; }; - 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 58F7D26427EB50A300E4D821 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = ""; }; 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = ""; }; 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerError.swift; sourceTree = ""; }; @@ -1393,12 +1480,12 @@ 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = ""; }; 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseButton.swift; sourceTree = ""; }; 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCheckOperation.swift; sourceTree = ""; }; - 58FE25D32AA729B5003D1918 /* ActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorTests.swift; sourceTree = ""; }; + 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorTests.swift; sourceTree = ""; }; 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorStub.swift; sourceTree = ""; }; 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelAdapterDummy.swift; sourceTree = ""; }; 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorStub.swift; sourceTree = ""; }; 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReaderStub.swift; sourceTree = ""; }; - 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Extensions.swift"; sourceTree = ""; }; + 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Extensions.swift"; sourceTree = ""; }; 58FE65922AB1CDE000E53CB5 /* DeviceCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheck.swift; sourceTree = ""; }; 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = ""; }; 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceChecker.swift; sourceTree = ""; }; @@ -1416,6 +1503,7 @@ 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = ""; }; 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = ""; }; 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = ""; }; + 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = ""; }; 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = ""; }; @@ -1448,9 +1536,19 @@ 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationCoordinator.swift; sourceTree = ""; }; 7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelCoordinator.swift; sourceTree = ""; }; 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = ""; }; + 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorProtocol.swift; sourceTree = ""; }; + 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = ""; }; + 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = ""; }; + 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyStub.swift; sourceTree = ""; }; 7AE47E512A17972A000418DA /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; + 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = ""; }; 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownStylingOptions.swift; sourceTree = ""; }; + A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountsProxy+Stubs.swift"; sourceTree = ""; }; + A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RESTRequestExecutor+Stubs.swift"; sourceTree = ""; }; + A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DevicesProxy+Stubs.swift"; sourceTree = ""; }; + A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIProxy+Stubs.swift"; sourceTree = ""; }; + A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenManager+Stubs.swift"; sourceTree = ""; }; A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = ""; }; A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = ""; }; A92ECC232A7802520052F1B1 /* StoredAccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredAccountData.swift; sourceTree = ""; }; @@ -1463,10 +1561,17 @@ A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadTransport.h; sourceTree = ""; }; A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = ""; }; A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = ""; }; + A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerTests.swift; sourceTree = ""; }; A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = ""; }; + A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelayCacheTracker+Stubs.swift"; sourceTree = ""; }; + A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerRelaysResponse+Stubs.swift"; sourceTree = ""; }; A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = ""; }; A9D96B192A8247C100A5C673 /* MigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = ""; }; A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportProvider.swift; sourceTree = ""; }; + A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; + A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Stubs.swift"; sourceTree = ""; }; + A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelStore+Stubs.swift"; sourceTree = ""; }; + A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusBlockObserver.swift; sourceTree = ""; }; A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; A9EC20E52A5C488D0040D56E /* Haversine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Haversine.swift; sourceTree = ""; }; A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesTests.swift; sourceTree = ""; }; @@ -1594,6 +1699,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A988DF242ADD307200D807EF /* libRelaySelector.a in Frameworks */, A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */, 58C7A43E2A863F470060C66F /* PacketTunnelCore.framework in Frameworks */, ); @@ -1838,6 +1944,8 @@ 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */, 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */, + A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */, + A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */, ); path = TunnelManager; sourceTree = ""; @@ -2192,26 +2300,27 @@ 5864AF802A9F52E3008BC928 /* Actor */ = { isa = PBXGroup; children = ( + 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */, + 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */, 583832282AC3DF1300EA2071 /* Command.swift */, 5838322A2AC3EF9600EA2071 /* CommandChannel.swift */, - 58E9C3852A4EF1CB00CFDEAC /* Actor.swift */, - 586C14592AC4735F00245C01 /* Actor+Public.swift */, - 583832222AC3181400EA2071 /* Actor+ErrorState.swift */, - 5838321E2AC3160A00EA2071 /* Actor+KeyPolicy.swift */, - 583832262AC3193600EA2071 /* Actor+SleepCycle.swift */, - 583832242AC318A100EA2071 /* Actor+ConnectionMonitoring.swift */, - 583832202AC3174700EA2071 /* Actor+NetworkReachability.swift */, - 58FE25F32AA9D730003D1918 /* Actor+Extensions.swift */, - 58DDA18E2ABC32380039C360 /* Timings.swift */, - 5824030C2A811B0000163DE8 /* State.swift */, - 58342C032AAB61FB003BA12D /* State+Extensions.swift */, 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */, - 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */, - 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */, - 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */, - 58ED3A132A7C199C0085CE65 /* StartOptions.swift */, 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */, + 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.swift */, + 583832242AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift */, + 583832222AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift */, + 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */, + 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */, + 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */, + 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */, + 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */, + 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */, 58E7A0312AA0715100C57861 /* Protocols */, + 58ED3A132A7C199C0085CE65 /* StartOptions.swift */, + 5824030C2A811B0000163DE8 /* State.swift */, + 58342C032AAB61FB003BA12D /* State+Extensions.swift */, + 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */, + 58DDA18E2ABC32380039C360 /* Timings.swift */, ); path = Actor; sourceTree = ""; @@ -2314,19 +2423,29 @@ 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = { isa = PBXGroup; children = ( - 58B0A2A4238EE67E00BC001D /* Info.plist */, + A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */, + A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */, A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */, + A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */, A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */, 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */, 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */, + A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */, 58FBFBF0291630700020E046 /* DurationTests.swift */, 58C3FA672A385C89006A450A /* FileCacheTests.swift */, 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */, + 58B0A2A4238EE67E00BC001D /* Info.plist */, F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, 58C3FA652A38549D006A450A /* MockFileCache.swift */, A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, + A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, + A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */, + A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */, 5807E2C1243203D000F5FF30 /* StringTests.swift */, + A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */, + A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */, + A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, ); path = MullvadVPNTests; @@ -2428,12 +2547,13 @@ 58C7A4432A863F490060C66F /* PacketTunnelCoreTests */ = { isa = PBXGroup; children = ( - 58FE25D32AA729B5003D1918 /* ActorTests.swift */, - 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */, + 58EC067D2A8D2B0700BEB973 /* Mocks */, + 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */, + 586C14572AC463BB00245C01 /* CommandChannelTests.swift */, + 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */, 58C7A46F2A8649ED0060C66F /* PingerTests.swift */, + 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */, 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */, - 586C14572AC463BB00245C01 /* CommandChannelTests.swift */, - 58EC067D2A8D2B0700BEB973 /* Mocks */, ); path = PacketTunnelCoreTests; sourceTree = ""; @@ -2441,6 +2561,7 @@ 58C9B8C52ABB23B400040B46 /* IPC */ = { isa = PBXGroup; children = ( + 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */, 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */, 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */, 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */, @@ -2456,6 +2577,7 @@ 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */, 5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */, 58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */, + 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */, ); path = URLRequestProxy; sourceTree = ""; @@ -2691,6 +2813,8 @@ 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */, 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */, 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */, + 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */, + 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */, ); path = Mocks; sourceTree = ""; @@ -2720,8 +2844,6 @@ isa = PBXGroup; children = ( 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, - 58F775422AB9E3EF00425B47 /* AppMessageHandler.swift */, - 585B1FF12AB0BC69008AD470 /* State+Extensions.swift */, 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */, 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */, @@ -3102,8 +3224,6 @@ buildRules = ( ); dependencies = ( - 58915D6C2A2603700066445B /* PBXTargetDependency */, - 062B45BF28FDA85D00746E77 /* PBXTargetDependency */, 06410DFA292C4ABC00AFC18C /* PBXTargetDependency */, ); name = MullvadVPNTests; @@ -3172,6 +3292,7 @@ buildRules = ( ); dependencies = ( + A988DF232ADD305300D807EF /* PBXTargetDependency */, 58C7A4402A863F470060C66F /* PBXTargetDependency */, 58C7A4722A864B860060C66F /* PBXTargetDependency */, ); @@ -3281,7 +3402,6 @@ ); dependencies = ( 58EED36E29FBEF040000CBAF /* PBXTargetDependency */, - 586A0DCE2A20E359006C731C /* PBXTargetDependency */, ); name = Operations; productName = Operations; @@ -3864,29 +3984,115 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 58C9B8D22ABB255100040B46 /* DeviceStateAccessor.swift in Sources */, - A9AD31D72A6AB68B00141BE8 /* InputTextFormatter.swift in Sources */, - 58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */, - 580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */, - 58C3FA662A38549D006A450A /* MockFileCache.swift in Sources */, - 7AF6E5F12A95F4A500F2679D /* DurationTests.swift in Sources */, - A9467E7F2A29DEFE000DC21F /* RelayCacheTests.swift in Sources */, - 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */, - 58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */, - 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, - 58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */, - 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, - 58C3FA682A385C89006A450A /* FileCacheTests.swift in Sources */, - 58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */, - 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, - 58C9B8D02ABB254000040B46 /* DeviceCheck.swift in Sources */, - 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */, - F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */, - A9EC20E82A5D3A8C0040D56E /* CoordinatesTests.swift in Sources */, - 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, - A9467E802A29E0A6000DC21F /* AddressCacheTests.swift in Sources */, - 58C9B8D32ABB255100040B46 /* DeviceCheckRemoteService.swift in Sources */, - 58C9B8D12ABB255100040B46 /* DeviceCheckOperation.swift in Sources */, + A9A5FA432ACB05F20083449F /* UIColor+Helpers.swift in Sources */, + A9A5FA3D2ACB05D90083449F /* DeviceCheck.swift in Sources */, + A900E9B82ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift in Sources */, + A9A5FA3E2ACB05D90083449F /* DeviceCheckOperation.swift in Sources */, + A9A5FA3F2ACB05D90083449F /* DeviceCheckRemoteService.swift in Sources */, + A9A5FA402ACB05D90083449F /* DeviceCheckRemoteServiceProtocol.swift in Sources */, + A9A5FA412ACB05D90083449F /* DeviceStateAccessor.swift in Sources */, + A9A5FA422ACB05D90083449F /* DeviceStateAccessorProtocol.swift in Sources */, + A9A5FA3C2ACB05B20083449F /* NSAttributedString+Markdown.swift in Sources */, + A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */, + A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */, + A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */, + A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */, + A9A5FA382ACB05600083449F /* InputTextFormatter.swift in Sources */, + A9A5FA372ACB052D0083449F /* ApplicationTarget.swift in Sources */, + A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */, + A900E9BC2ACC609200C95F67 /* DevicesProxy+Stubs.swift in Sources */, + A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */, + A9A5F9E32ACB05160083449F /* AccountDataThrottling.swift in Sources */, + A9A5F9E42ACB05160083449F /* AppPreferences.swift in Sources */, + A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */, + A9A5F9E62ACB05160083449F /* DeviceDataThrottling.swift in Sources */, + A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */, + A9A5F9E82ACB05160083449F /* MarkdownStylingOptions.swift in Sources */, + A9A5F9E92ACB05160083449F /* ObserverList.swift in Sources */, + A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */, + A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */, + A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */, + A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */, + A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, + A9A5F9EF2ACB05160083449F /* String+AccountFormatting.swift in Sources */, + A9A5F9F02ACB05160083449F /* String+FuzzyMatch.swift in Sources */, + A9A5F9F12ACB05160083449F /* String+Split.swift in Sources */, + A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */, + A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */, + A9A5F9F42ACB05160083449F /* AccountExpiryInAppNotificationProvider.swift in Sources */, + A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */, + A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */, + A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */, + A9A5F9F82ACB05160083449F /* NotificationProviderIdentifier.swift in Sources */, + A9A5F9F92ACB05160083449F /* NotificationProvider.swift in Sources */, + A9A5F9FA2ACB05160083449F /* InAppNotificationDescriptor.swift in Sources */, + A9A5F9FB2ACB05160083449F /* InAppNotificationProvider.swift in Sources */, + A9A5F9FC2ACB05160083449F /* SystemNotificationProvider.swift in Sources */, + A9A5F9FD2ACB05160083449F /* NotificationResponse.swift in Sources */, + A9A5F9FE2ACB05160083449F /* NotificationManager.swift in Sources */, + A9A5F9FF2ACB05160083449F /* NotificationManagerDelegate.swift in Sources */, + A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */, + A900E9BA2ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift in Sources */, + A9A5FA002ACB05160083449F /* ProductsRequestOperation.swift in Sources */, + A9A5FA012ACB05160083449F /* RelayCacheTrackerObserver.swift in Sources */, + A9A5FA022ACB05160083449F /* RelayCacheTracker.swift in Sources */, + A9A5FA032ACB05160083449F /* SimulatorTunnelInfo.swift in Sources */, + A9A5FA042ACB05160083449F /* SimulatorTunnelProvider.swift in Sources */, + A9A5FA052ACB05160083449F /* SimulatorTunnelProviderHost.swift in Sources */, + A900E9C02ACC661900C95F67 /* AccessTokenManager+Stubs.swift in Sources */, + A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */, + A9A5FA062ACB05160083449F /* SimulatorTunnelProviderManager.swift in Sources */, + A9A5FA072ACB05160083449F /* SimulatorVPNConnection.swift in Sources */, + A9A5FA082ACB05160083449F /* StorePaymentBlockObserver.swift in Sources */, + A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */, + A9A5FA092ACB05160083449F /* SendStoreReceiptOperation.swift in Sources */, + A9A5FA0A2ACB05160083449F /* StorePaymentEvent.swift in Sources */, + A9A5FA0B2ACB05160083449F /* StorePaymentManager.swift in Sources */, + A9A5FA0C2ACB05160083449F /* StorePaymentManagerDelegate.swift in Sources */, + A9A5FA0D2ACB05160083449F /* StorePaymentManagerError.swift in Sources */, + A9A5FA0E2ACB05160083449F /* StorePaymentObserver.swift in Sources */, + A9A5FA0F2ACB05160083449F /* StoreSubscription.swift in Sources */, + A9A5FA102ACB05160083449F /* PacketTunnelTransport.swift in Sources */, + A9A5FA112ACB05160083449F /* TransportMonitor.swift in Sources */, + A9A5FA122ACB05160083449F /* DeleteAccountOperation.swift in Sources */, + A9A5FA132ACB05160083449F /* LoadTunnelConfigurationOperation.swift in Sources */, + A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */, + A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */, + A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */, + A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */, + A9A5FA182ACB05160083449F /* SetAccountOperation.swift in Sources */, + A9A5FA192ACB05160083449F /* StartTunnelOperation.swift in Sources */, + A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */, + A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */, + A9A5FA1C2ACB05160083449F /* Tunnel+Messaging.swift in Sources */, + A9A5FA1D2ACB05160083449F /* TunnelBlockObserver.swift in Sources */, + A9A5FA1E2ACB05160083449F /* TunnelConfiguration.swift in Sources */, + A9A5FA1F2ACB05160083449F /* TunnelInteractor.swift in Sources */, + A9A5FA202ACB05160083449F /* TunnelManager.swift in Sources */, + A9A5FA212ACB05160083449F /* TunnelManagerErrors.swift in Sources */, + A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */, + A9A5FA222ACB05160083449F /* TunnelObserver.swift in Sources */, + A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */, + A9A5FA232ACB05160083449F /* TunnelState.swift in Sources */, + A9A5FA242ACB05160083449F /* TunnelStore.swift in Sources */, + A9A5FA252ACB05160083449F /* UpdateAccountDataOperation.swift in Sources */, + A9A5FA262ACB05160083449F /* UpdateDeviceDataOperation.swift in Sources */, + A9A5FA272ACB05160083449F /* VPNConnectionProtocol.swift in Sources */, + A9A5FA282ACB05160083449F /* WgKeyRotation.swift in Sources */, + A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */, + A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */, + A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */, + A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */, + A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */, + A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */, + A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */, + A9A5FA302ACB05160083449F /* InputTextFormatterTests.swift in Sources */, + A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */, + A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */, + A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */, + A9A5FA342ACB05160083449F /* StringTests.swift in Sources */, + A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */, + A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3916,40 +4122,43 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 58FE25F42AA9D730003D1918 /* Actor+Extensions.swift in Sources */, + 58FE25F42AA9D730003D1918 /* PacketTunnelActor+Extensions.swift in Sources */, 58DDA18F2ABC32380039C360 /* Timings.swift in Sources */, 58FE25DF2AA72A9B003D1918 /* RelaySelectorProtocol.swift in Sources */, 58C7A4522A863FB50060C66F /* Pinger.swift in Sources */, 580D6B8C2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift in Sources */, 58C7AF172ABD84AA007EDD7A /* ProxyURLRequest.swift in Sources */, - 5838321F2AC3160A00EA2071 /* Actor+KeyPolicy.swift in Sources */, + 5838321F2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift in Sources */, 58C7AF122ABD8480007EDD7A /* TunnelProviderReply.swift in Sources */, 58C7A4572A863FB90060C66F /* TunnelDeviceInfoProtocol.swift in Sources */, 58C7A4562A863FB90060C66F /* DefaultPathObserverProtocol.swift in Sources */, - 58FE25DA2AA72A8F003D1918 /* Actor.swift in Sources */, + 58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */, 58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */, - 583832252AC318A100EA2071 /* Actor+ConnectionMonitoring.swift in Sources */, + 583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */, 58C7A4552A863FB90060C66F /* TunnelMonitor.swift in Sources */, 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */, 58C7A4512A863FB50060C66F /* PingerProtocol.swift in Sources */, 583832292AC3DF1300EA2071 /* Command.swift in Sources */, - 583832232AC3181400EA2071 /* Actor+ErrorState.swift in Sources */, + 583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */, 58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */, 58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */, 58FE25D72AA72A8F003D1918 /* State.swift in Sources */, 58C7AF132ABD8480007EDD7A /* PacketTunnelStatus.swift in Sources */, 58C7A4592A863FB90060C66F /* WgStats.swift in Sources */, + 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */, 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */, 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */, - 583832212AC3174700EA2071 /* Actor+NetworkReachability.swift in Sources */, + 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */, 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */, + 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */, 580D6B8A2AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift in Sources */, + 7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */, 5826B6CB2ABD83E200B1CA13 /* PacketTunnelOptions.swift in Sources */, 586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */, 5838322B2AC3EF9600EA2071 /* CommandChannel.swift in Sources */, - 586C145A2AC4735F00245C01 /* Actor+Public.swift in Sources */, + 586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */, 58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */, - 583832272AC3193600EA2071 /* Actor+SleepCycle.swift in Sources */, + 583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */, 58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */, 58C7AF152ABD8480007EDD7A /* PacketTunnelRelay.swift in Sources */, 58FE25D92AA72A8F003D1918 /* AutoCancellingTask.swift in Sources */, @@ -3962,20 +4171,26 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7AD0AA1C2AD6A63F00119E10 /* PacketTunnelActorStub.swift in Sources */, 58EC067A2A8D208D00BEB973 /* TunnelDeviceInfoStub.swift in Sources */, 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */, 58EC067C2A8D2A0B00BEB973 /* NetworkCounters.swift in Sources */, 58FE25EC2AA77639003D1918 /* TunnelMonitorStub.swift in Sources */, + 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */, 58FE25EE2AA7764E003D1918 /* TunnelAdapterDummy.swift in Sources */, 581F23AD2A8CF92100788AB6 /* DefaultPathObserverFake.swift in Sources */, 5838321B2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift in Sources */, 5838321D2AC1C54600EA2071 /* TaskSleepTests.swift in Sources */, 58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */, + 7AD0AA212AD6CB0000119E10 /* URLRequestProxyStub.swift in Sources */, 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */, 581F23AF2A8CF94D00788AB6 /* PingerMock.swift in Sources */, + 7A3FD1B62AD542110042BEA6 /* ServerRelaysResponse+Stubs.swift in Sources */, + 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */, 58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */, 58F7753D2AB8473200425B47 /* BlockedStateErrorMapperStub.swift in Sources */, - 58FE25D42AA729B5003D1918 /* ActorTests.swift in Sources */, + 58FE25D42AA729B5003D1918 /* PacketTunnelActorTests.swift in Sources */, + 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */, 58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4183,6 +4398,7 @@ 5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */, 7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */, 063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */, + A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, 5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */, @@ -4217,7 +4433,6 @@ 581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */, 580810E52A30E13A00B74552 /* DeviceStateAccessorProtocol.swift in Sources */, 580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */, - 585B1FF22AB0BC69008AD470 /* State+Extensions.swift in Sources */, 58C9B8CE2ABB252E00040B46 /* DeviceCheck.swift in Sources */, 58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */, 58E9C3842A4EF15300CFDEAC /* WireGuardAdapter+Async.swift in Sources */, @@ -4237,7 +4452,6 @@ 58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */, 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, 582403822A827E1500163DE8 /* RelaySelectorWrapper.swift in Sources */, - 58F775432AB9E3EF00425B47 /* AppMessageHandler.swift in Sources */, 58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4288,6 +4502,7 @@ 7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */, 58D2240A294C90210029F5F8 /* IPAddress+Codable.swift in Sources */, 58E45A5729F12C5100281ECF /* Result+Extensions.swift in Sources */, + A9E031782ACB09930095D843 /* UIApplication+Extensions.swift in Sources */, 58D2240B294C90210029F5F8 /* Cancellable.swift in Sources */, 58D2240C294C90210029F5F8 /* WrappingError.swift in Sources */, A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */, @@ -4325,7 +4540,7 @@ buildActionMask = 2147483647; files = ( 58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */, - A9A1DE7A2AD5709A0073F689 /* RESTTransportStrategy.swift in Sources */, + A988DF212ADD293D00D807EF /* RESTTransportStrategy.swift in Sources */, A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */, 58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */, 58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */, @@ -4378,11 +4593,6 @@ target = 06799ABB28F98E1D00ACD94E /* MullvadREST */; targetProxy = 58D2239E294C89B50029F5F8 /* PBXContainerItemProxy */; }; - 062B45BF28FDA85D00746E77 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 06799ABB28F98E1D00ACD94E /* MullvadREST */; - targetProxy = 58D2239F294C89B50029F5F8 /* PBXContainerItemProxy */; - }; 063F02782902B63F001FA09F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 063F02722902B63F001FA09F /* RelayCache */; @@ -4428,20 +4638,11 @@ target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */; targetProxy = 58695AA22A4ADA9200328DB3 /* PBXContainerItemProxy */; }; - 586A0DCE2A20E359006C731C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; - targetProxy = 586A0DCD2A20E359006C731C /* PBXContainerItemProxy */; - }; 586A0DD72A20E4A9006C731C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 06799ABB28F98E1D00ACD94E /* MullvadREST */; targetProxy = 586A0DD62A20E4A9006C731C /* PBXContainerItemProxy */; }; - 58915D6C2A2603700066445B /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = 58915D6B2A2603700066445B /* WireGuardKitTypes */; - }; 58B2FDD82AA71D2A003EB5C6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 58B2FDD22AA71D2A003EB5C6 /* MullvadSettings */; @@ -4610,6 +4811,11 @@ target = A97F1F402A1F4E1A00ECEFDE /* MullvadTransport */; targetProxy = A97F1F452A1F4E1A00ECEFDE /* PBXContainerItemProxy */; }; + A988DF232ADD305300D807EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5898D29729017DAC00EB5EBA /* RelaySelector */; + targetProxy = A988DF222ADD305300D807EF /* PBXContainerItemProxy */; + }; A9B2CF712A1F64B20013CC6C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 06799ABB28F98E1D00ACD94E /* MullvadREST */; @@ -6174,11 +6380,6 @@ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; productName = WireGuardKitTypes; }; - 58915D6B2A2603700066445B /* WireGuardKitTypes */ = { - isa = XCSwiftPackageProductDependency; - package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; - productName = WireGuardKitTypes; - }; 58915D6D2A26037A0066445B /* WireGuardKitTypes */ = { isa = XCSwiftPackageProductDependency; package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; diff --git a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift index 8c1dc2b25894..2734729c46a4 100644 --- a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift @@ -24,7 +24,7 @@ final class AddressCacheTracker { private let application: UIApplication /// REST API proxy. - private let apiProxy: REST.APIProxy + private let apiProxy: APIQuerying /// Address cache. private let store: REST.AddressCache @@ -45,7 +45,7 @@ final class AddressCacheTracker { private let nslock = NSLock() /// Designated initializer - init(application: UIApplication, apiProxy: REST.APIProxy, store: REST.AddressCache) { + init(application: UIApplication, apiProxy: APIQuerying, store: REST.AddressCache) { self.application = application self.apiProxy = apiProxy self.store = store diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index c9f208523cca..9e9e1cc9c0bf 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -33,9 +33,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private(set) var addressCache: REST.AddressCache! private var proxyFactory: REST.ProxyFactory! - private(set) var apiProxy: REST.APIProxy! - private(set) var accountsProxy: REST.AccountsProxy! - private(set) var devicesProxy: REST.DevicesProxy! + private(set) var apiProxy: APIQuerying! + private(set) var accountsProxy: RESTAccountHandling! + private(set) var devicesProxy: DeviceHandling! private(set) var addressCacheTracker: AddressCacheTracker! private(set) var relayCacheTracker: RelayCacheTracker! diff --git a/ios/MullvadVPN/Classes/ObserverList.swift b/ios/MullvadVPN/Classes/ObserverList.swift index 70ace90b69bc..45123d39a4a9 100644 --- a/ios/MullvadVPN/Classes/ObserverList.swift +++ b/ios/MullvadVPN/Classes/ObserverList.swift @@ -23,8 +23,8 @@ struct WeakBox { } } - static func == (lhs: WeakBox, rhs: T) -> Bool { - (lhs.value as AnyObject) === (rhs as AnyObject) + static func == (lhs: WeakBox, rhs: WeakBox) -> Bool { + (lhs.value as AnyObject) === (rhs.value as AnyObject) } } @@ -36,7 +36,7 @@ final class ObserverList { lock.lock() let hasObserver = observers.contains { box in - box == observer + box == WeakBox(observer) } if !hasObserver { @@ -50,7 +50,7 @@ final class ObserverList { lock.lock() let index = observers.firstIndex { box in - box == observer + box == WeakBox(observer) } if let index { diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 3de34f1515a2..a58603fb041a 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -72,9 +72,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private let storePaymentManager: StorePaymentManager private let relayCacheTracker: RelayCacheTracker - private let apiProxy: REST.APIProxy - private let devicesProxy: REST.DevicesProxy - private let accountsProxy: REST.AccountsProxy + private let apiProxy: APIQuerying + private let devicesProxy: DeviceHandling + private let accountsProxy: RESTAccountHandling private var tunnelObserver: TunnelObserver? private var appPreferences: AppPreferencesDataSource @@ -88,9 +88,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo tunnelManager: TunnelManager, storePaymentManager: StorePaymentManager, relayCacheTracker: RelayCacheTracker, - apiProxy: REST.APIProxy, - devicesProxy: REST.DevicesProxy, - accountsProxy: REST.AccountsProxy, + apiProxy: APIQuerying, + devicesProxy: DeviceHandling, + accountsProxy: RESTAccountHandling, appPreferences: AppPreferencesDataSource ) { self.tunnelManager = tunnelManager diff --git a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift index f62869f27177..42ddd0e2a7c7 100644 --- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift @@ -15,7 +15,7 @@ import UIKit final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewControllerDelegate { private let tunnelManager: TunnelManager - private let devicesProxy: REST.DevicesProxy + private let devicesProxy: DeviceHandling private var loginController: LoginViewController? private var lastLoginAction: LoginAction? @@ -34,7 +34,7 @@ final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewContr init( navigationController: RootContainerViewController, tunnelManager: TunnelManager, - devicesProxy: REST.DevicesProxy + devicesProxy: DeviceHandling ) { self.navigationController = navigationController self.tunnelManager = tunnelManager diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index a614dd780f53..ec6b9a30a850 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -17,7 +17,7 @@ final class WelcomeCoordinator: Coordinator, Poppable, Presenting { private let storePaymentManager: StorePaymentManager private let tunnelManager: TunnelManager private let inAppPurchaseInteractor: InAppPurchaseInteractor - private let accountsProxy: REST.AccountsProxy + private let accountsProxy: RESTAccountHandling private var viewController: WelcomeViewController? @@ -32,7 +32,7 @@ final class WelcomeCoordinator: Coordinator, Poppable, Presenting { navigationController: RootContainerViewController, storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager, - accountsProxy: REST.AccountsProxy + accountsProxy: RESTAccountHandling ) { self.navigationController = navigationController self.storePaymentManager = storePaymentManager diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index 5a16d473a16a..7f17a5b8da9d 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -14,7 +14,17 @@ import Operations import RelayCache import UIKit -final class RelayCacheTracker { +protocol RelayCacheTrackerProtocol { + func startPeriodicUpdates() + func stopPeriodicUpdates() + func updateRelays(completionHandler: ((Result) -> Void)?) -> Cancellable + func getCachedRelays() throws -> CachedRelays + func getNextUpdateDate() -> Date + func addObserver(_ observer: RelayCacheTrackerObserver) + func removeObserver(_ observer: RelayCacheTrackerObserver) +} + +final class RelayCacheTracker: RelayCacheTrackerProtocol { /// Relay update interval. static let relayUpdateInterval: Duration = .hours(1) @@ -22,7 +32,7 @@ final class RelayCacheTracker { private let logger = Logger(label: "RelayCacheTracker") /// Relay cache. - private let cache: RelayCache + private let cache: RelayCacheProtocol private let application: UIApplication @@ -39,7 +49,7 @@ final class RelayCacheTracker { private var isPeriodicUpdatesEnabled = false /// API proxy. - private let apiProxy: REST.APIProxy + private let apiProxy: APIQuerying /// Observers. private let observerList = ObserverList() @@ -47,7 +57,7 @@ final class RelayCacheTracker { /// Memory cache. private var cachedRelays: CachedRelays? - init(relayCache: RelayCache, application: UIApplication, apiProxy: REST.APIProxy) { + init(relayCache: RelayCacheProtocol, application: UIApplication, apiProxy: APIQuerying) { self.application = application self.apiProxy = apiProxy cache = relayCache diff --git a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift index f2822ec79e20..d29bde3d9db4 100644 --- a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift +++ b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift @@ -14,7 +14,7 @@ import Operations import StoreKit class SendStoreReceiptOperation: ResultOperation, SKRequestDelegate { - private let apiProxy: REST.APIProxy + private let apiProxy: APIQuerying private let accountNumber: String private let forceRefresh: Bool @@ -26,7 +26,7 @@ class SendStoreReceiptOperation: ResultOperation() private weak var classDelegate: StorePaymentManagerDelegate? @@ -66,8 +66,8 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { init( application: UIApplication, queue: SKPaymentQueue, - apiProxy: REST.APIProxy, - accountsProxy: REST.AccountsProxy + apiProxy: APIQuerying, + accountsProxy: RESTAccountHandling ) { self.application = application paymentQueue = queue diff --git a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift index c9621be8ac2f..15af753a6a3d 100644 --- a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift +++ b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift @@ -18,9 +18,9 @@ struct PacketTunnelTransport: RESTTransport { "packet-tunnel" } - let tunnel: Tunnel + let tunnel: any TunnelProtocol - init(tunnel: Tunnel) { + init(tunnel: any TunnelProtocol) { self.tunnel = tunnel } diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift index 99a59cc1165c..a89a02977ca7 100644 --- a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift +++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift @@ -47,7 +47,7 @@ final class TransportMonitor: RESTTransportProvider { } } - private func shouldByPassVPN(tunnel: Tunnel) -> Bool { + private func shouldByPassVPN(tunnel: any TunnelProtocol) -> Bool { switch tunnel.status { case .connected: return tunnelManager.isConfigurationLoaded && tunnelManager.deviceState == .revoked diff --git a/ios/MullvadVPN/TunnelManager/DeleteAccountOperation.swift b/ios/MullvadVPN/TunnelManager/DeleteAccountOperation.swift index ad4463d5622a..8731ca2a5cf1 100644 --- a/ios/MullvadVPN/TunnelManager/DeleteAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/DeleteAccountOperation.swift @@ -16,14 +16,14 @@ class DeleteAccountOperation: ResultOperation { private let logger = Logger(label: "\(DeleteAccountOperation.self)") private let accountNumber: String - private let accountsProxy: REST.AccountsProxy - private let accessTokenManager: REST.AccessTokenManager + private let accountsProxy: RESTAccountHandling + private let accessTokenManager: RESTAccessTokenManagement private var task: Cancellable? init( dispatchQueue: DispatchQueue, - accountsProxy: REST.AccountsProxy, - accessTokenManager: REST.AccessTokenManager, + accountsProxy: RESTAccountHandling, + accessTokenManager: RESTAccessTokenManagement, accountNumber: String ) { self.accountNumber = accountNumber diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift index d4c86a74f101..54f14355a73e 100644 --- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift +++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift @@ -51,7 +51,7 @@ class LoadTunnelConfigurationOperation: ResultOperation { } } - private func finishOperation(tunnel: Tunnel?) { + private func finishOperation(tunnel: (any TunnelProtocol)?) { interactor.setTunnel(tunnel, shouldRefreshTunnelState: true) interactor.setConfigurationLoaded() diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index 78541be833b7..5282a2a7d57a 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -94,7 +94,7 @@ class MapConnectionStatusOperation: AsyncOperation { request?.cancel() } - private func handleConnectingState(_ tunnelState: TunnelState, _ tunnel: Tunnel) { + private func handleConnectingState(_ tunnelState: TunnelState, _ tunnel: any TunnelProtocol) { switch tunnelState { case .connecting: break @@ -160,7 +160,7 @@ class MapConnectionStatusOperation: AsyncOperation { } private func fetchTunnelStatus( - tunnel: Tunnel, + tunnel: any TunnelProtocol, mapToState: @escaping (PacketTunnelStatus) -> TunnelState? ) { request = tunnel.getTunnelStatus { [weak self] completion in diff --git a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift index 7fab35b2e4be..99d77a1204a0 100644 --- a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift +++ b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift @@ -18,14 +18,14 @@ class RedeemVoucherOperation: ResultOperation { private let interactor: TunnelInteractor private let voucherCode: String - private let apiProxy: REST.APIProxy + private let apiProxy: APIQuerying private var task: Cancellable? init( dispatchQueue: DispatchQueue, interactor: TunnelInteractor, voucherCode: String, - apiProxy: REST.APIProxy + apiProxy: APIQuerying ) { self.interactor = interactor self.voucherCode = voucherCode diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift index b40968d380c5..46b340ec9d5e 100644 --- a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift +++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift @@ -17,10 +17,10 @@ import class WireGuardKitTypes.PrivateKey class RotateKeyOperation: ResultOperation { private let logger = Logger(label: "RotateKeyOperation") private let interactor: TunnelInteractor - private let devicesProxy: REST.DevicesProxy + private let devicesProxy: DeviceHandling private var task: Cancellable? - init(dispatchQueue: DispatchQueue, interactor: TunnelInteractor, devicesProxy: REST.DevicesProxy) { + init(dispatchQueue: DispatchQueue, interactor: TunnelInteractor, devicesProxy: DeviceHandling) { self.interactor = interactor self.devicesProxy = devicesProxy diff --git a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift index d34e51bdbfd6..cb9afb10dd01 100644 --- a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift @@ -24,7 +24,7 @@ final class SendTunnelProviderMessageOperation: ResultOperation typealias DecoderHandler = (Data?) throws -> Output private let application: UIApplication - private let tunnel: Tunnel + private let tunnel: any TunnelProtocol private let message: TunnelProviderMessage private let timeout: Duration @@ -39,7 +39,7 @@ final class SendTunnelProviderMessageOperation: ResultOperation init( dispatchQueue: DispatchQueue, application: UIApplication, - tunnel: Tunnel, + tunnel: any TunnelProtocol, message: TunnelProviderMessage, timeout: Duration? = nil, decoderHandler: @escaping DecoderHandler, @@ -219,7 +219,7 @@ extension SendTunnelProviderMessageOperation where Output: Codable { convenience init( dispatchQueue: DispatchQueue, application: UIApplication, - tunnel: Tunnel, + tunnel: any TunnelProtocol, message: TunnelProviderMessage, timeout: Duration? = nil, completionHandler: @escaping CompletionHandler @@ -246,7 +246,7 @@ extension SendTunnelProviderMessageOperation where Output == Void { convenience init( dispatchQueue: DispatchQueue, application: UIApplication, - tunnel: Tunnel, + tunnel: any TunnelProtocol, message: TunnelProviderMessage, timeout: Duration? = nil, completionHandler: CompletionHandler? diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index ee805c9d92c9..97a9c6d75bc5 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -27,22 +27,19 @@ enum SetAccountAction { var taskName: String { switch self { - case .new: - return "Set new account" - case .existing: - return "Set existing account" - case .unset: - return "Unset account" + case .new: "Set new account" + case .existing: "Set existing account" + case .unset: "Unset account" } } } class SetAccountOperation: ResultOperation { private let interactor: TunnelInteractor - private let accountsProxy: REST.AccountsProxy - private let devicesProxy: REST.DevicesProxy + private let accountsProxy: RESTAccountHandling + private let devicesProxy: DeviceHandling private let action: SetAccountAction - private let accessTokenManager: REST.AccessTokenManager + private let accessTokenManager: RESTAccessTokenManagement private let logger = Logger(label: "SetAccountOperation") private var tasks: [Cancellable] = [] @@ -50,9 +47,9 @@ class SetAccountOperation: ResultOperation { init( dispatchQueue: DispatchQueue, interactor: TunnelInteractor, - accountsProxy: REST.AccountsProxy, - devicesProxy: REST.DevicesProxy, - accessTokenManager: REST.AccessTokenManager, + accountsProxy: RESTAccountHandling, + devicesProxy: DeviceHandling, + accessTokenManager: RESTAccessTokenManagement, action: SetAccountAction ) { self.interactor = interactor diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index afba76a88790..86dce49a3a9e 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -85,7 +85,7 @@ class StartTunnelOperation: ResultOperation { } } - private func startTunnel(tunnel: Tunnel, selectedRelay: SelectedRelay) throws { + private func startTunnel(tunnel: any TunnelProtocol, selectedRelay: SelectedRelay) throws { var tunnelOptions = PacketTunnelOptions() do { @@ -108,7 +108,7 @@ class StartTunnelOperation: ResultOperation { try tunnel.start(options: tunnelOptions.rawOptions()) } - private func makeTunnelProvider(completionHandler: @escaping (Result) -> Void) { + private func makeTunnelProvider(completionHandler: @escaping (Result) -> Void) { let persistentTunnels = interactor.getPersistentTunnels() let tunnel = persistentTunnels.first ?? interactor.createNewTunnel() let configuration = Self.makeTunnelConfiguration() diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift index 0eb06130d82d..044901c37de0 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift @@ -22,7 +22,7 @@ private let dispatchQueue = DispatchQueue(label: "Tunnel.dispatchQueue") /// Timeout for proxy requests. private let proxyRequestTimeout = REST.defaultAPINetworkTimeout + 2 -extension Tunnel { +extension TunnelProtocol { /// Request packet tunnel process to reconnect the tunnel with the given relay. func reconnectTunnel( to nextRelay: NextRelay, diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index 5a9990c8862d..1ff8f9179b1b 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -17,11 +17,36 @@ typealias TunnelProviderManagerType = NETunnelProviderManager #endif protocol TunnelStatusObserver { - func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) + func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) +} + +protocol TunnelProtocol: AnyObject { + var status: NEVPNStatus { get } + var isOnDemandEnabled: Bool { get set } + var startDate: Date? { get } + + init(tunnelProvider: TunnelProviderManagerType) + + func addObserver(_ observer: any TunnelStatusObserver) + func removeObserver(_ observer: any TunnelStatusObserver) + func addBlockObserver( + queue: DispatchQueue?, + handler: @escaping (any TunnelProtocol, NEVPNStatus) -> Void + ) -> TunnelStatusBlockObserver + + func logFormat() -> String + + func saveToPreferences(_ completion: @escaping (Error?) -> Void) + func removeFromPreferences(completion: @escaping (Error?) -> Void) + + func setConfiguration(_ configuration: TunnelConfiguration) + func start(options: [String: NSObject]?) throws + func stop() + func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws } /// Tunnel wrapper class. -final class Tunnel: Equatable { +final class Tunnel: TunnelProtocol, Equatable { /// Unique identifier assigned to instance at the time of creation. let identifier = UUID() @@ -82,10 +107,10 @@ final class Tunnel: Equatable { } private let lock = NSLock() - private var observerList = ObserverList() + private var observerList = ObserverList() private var _startDate: Date? - private let tunnelProvider: TunnelProviderManagerType + internal let tunnelProvider: TunnelProviderManagerType init(tunnelProvider: TunnelProviderManagerType) { self.tunnelProvider = tunnelProvider @@ -137,7 +162,7 @@ final class Tunnel: Equatable { func addBlockObserver( queue: DispatchQueue? = nil, - handler: @escaping (Tunnel, NEVPNStatus) -> Void + handler: @escaping (any TunnelProtocol, NEVPNStatus) -> Void ) -> TunnelStatusBlockObserver { let observer = TunnelStatusBlockObserver(tunnel: self, queue: queue, handler: handler) @@ -146,11 +171,11 @@ final class Tunnel: Equatable { return observer } - func addObserver(_ observer: TunnelStatusObserver) { + func addObserver(_ observer: any TunnelStatusObserver) { observerList.append(observer) } - func removeObserver(_ observer: TunnelStatusObserver) { + func removeObserver(_ observer: any TunnelStatusObserver) { observerList.remove(observer) } @@ -197,33 +222,3 @@ final class Tunnel: Equatable { lhs.tunnelProvider == rhs.tunnelProvider } } - -final class TunnelStatusBlockObserver: TunnelStatusObserver { - typealias Handler = (Tunnel, NEVPNStatus) -> Void - - private weak var tunnel: Tunnel? - private let queue: DispatchQueue? - private let handler: Handler - - fileprivate init(tunnel: Tunnel, queue: DispatchQueue?, handler: @escaping Handler) { - self.tunnel = tunnel - self.queue = queue - self.handler = handler - } - - func invalidate() { - tunnel?.removeObserver(self) - } - - func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) { - let block = { - self.handler(tunnel, status) - } - - if let queue { - queue.async(execute: block) - } else { - block() - } - } -} diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index 26f40bc0d4f0..15f118f0352e 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -13,11 +13,11 @@ import PacketTunnelCore protocol TunnelInteractor { // MARK: - Tunnel manipulation - var tunnel: Tunnel? { get } + var tunnel: (any TunnelProtocol)? { get } - func getPersistentTunnels() -> [Tunnel] - func createNewTunnel() -> Tunnel - func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) + func getPersistentTunnels() -> [any TunnelProtocol] + func createNewTunnel() -> any TunnelProtocol + func setTunnel(_ tunnel: (any TunnelProtocol)?, shouldRefreshTunnelState: Bool) // MARK: - Tunnel status diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 34cfca0d1aae..ab178dba380d 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -44,13 +44,13 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Internal variables - private let application: UIApplication - fileprivate let tunnelStore: TunnelStore - private let relayCacheTracker: RelayCacheTracker - private let accountsProxy: REST.AccountsProxy - private let devicesProxy: REST.DevicesProxy - private let apiProxy: REST.APIProxy - private let accessTokenManager: REST.AccessTokenManager + private let application: BackgroundTaskProvider + fileprivate let tunnelStore: any TunnelStoreProtocol + private let relayCacheTracker: RelayCacheTrackerProtocol + private let accountsProxy: RESTAccountHandling + private let devicesProxy: DeviceHandling + private let apiProxy: APIQuerying + private let accessTokenManager: RESTAccessTokenManagement private let logger = Logger(label: "TunnelManager") private var nslock = NSRecursiveLock() @@ -72,7 +72,7 @@ final class TunnelManager: StorePaymentObserver { private var _deviceState: DeviceState = .loggedOut private var _tunnelSettings = LatestTunnelSettings() - private var _tunnel: Tunnel? + private var _tunnel: (any TunnelProtocol)? private var _tunnelStatus = TunnelStatus() /// Last processed device check. @@ -81,13 +81,13 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Initialization init( - application: UIApplication, - tunnelStore: TunnelStore, - relayCacheTracker: RelayCacheTracker, - accountsProxy: REST.AccountsProxy, - devicesProxy: REST.DevicesProxy, - apiProxy: REST.APIProxy, - accessTokenManager: REST.AccessTokenManager + application: BackgroundTaskProvider, + tunnelStore: any TunnelStoreProtocol, + relayCacheTracker: RelayCacheTrackerProtocol, + accountsProxy: RESTAccountHandling, + devicesProxy: DeviceHandling, + apiProxy: APIQuerying, + accessTokenManager: RESTAccessTokenManagement ) { self.application = application self.tunnelStore = tunnelStore @@ -289,7 +289,7 @@ final class TunnelManager: StorePaymentObserver { finish(result.error) } } catch { - if let error = error as? NoRelaysSatisfyingConstraintsError { + if error is NoRelaysSatisfyingConstraintsError { _ = self.setTunnelStatus { tunnelStatus in tunnelStatus.state = .error(.noRelaysSatisfyingConstraints) } @@ -623,7 +623,7 @@ final class TunnelManager: StorePaymentObserver { return _isConfigurationLoaded } - fileprivate var tunnel: Tunnel? { + fileprivate var tunnel: (any TunnelProtocol)? { nslock.lock() defer { nslock.unlock() } @@ -668,7 +668,7 @@ final class TunnelManager: StorePaymentObserver { } } - fileprivate func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) { + fileprivate func setTunnel(_ tunnel: (any TunnelProtocol)?, shouldRefreshTunnelState: Bool) { nslock.lock() defer { nslock.unlock() } @@ -867,7 +867,7 @@ final class TunnelManager: StorePaymentObserver { } } - private func subscribeVPNStatusObserver(tunnel: Tunnel) { + private func subscribeVPNStatusObserver(tunnel: any TunnelProtocol) { nslock.lock() defer { nslock.unlock() } @@ -1255,19 +1255,19 @@ private struct TunnelInteractorProxy: TunnelInteractor { self.tunnelManager = tunnelManager } - var tunnel: Tunnel? { + var tunnel: (any TunnelProtocol)? { tunnelManager.tunnel } - func getPersistentTunnels() -> [Tunnel] { + func getPersistentTunnels() -> [any TunnelProtocol] { tunnelManager.tunnelStore.getPersistentTunnels() } - func createNewTunnel() -> Tunnel { + func createNewTunnel() -> any TunnelProtocol { tunnelManager.tunnelStore.createNewTunnel() } - func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) { + func setTunnel(_ tunnel: (any TunnelProtocol)?, shouldRefreshTunnelState: Bool) { tunnelManager.setTunnel(tunnel, shouldRefreshTunnelState: shouldRefreshTunnelState) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift new file mode 100644 index 000000000000..135e79220c7a --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift @@ -0,0 +1,40 @@ +// +// TunnelStatusBlockObserver.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension + +final class TunnelStatusBlockObserver: TunnelStatusObserver { + typealias Handler = (any TunnelProtocol, NEVPNStatus) -> Void + + private weak var tunnel: (any TunnelProtocol)? + private let queue: DispatchQueue? + private let handler: Handler + + init(tunnel: any TunnelProtocol, queue: DispatchQueue?, handler: @escaping Handler) { + self.tunnel = tunnel + self.queue = queue + self.handler = handler + } + + func invalidate() { + tunnel?.removeObserver(self) + } + + func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) { + let block = { + self.handler(tunnel, status) + } + + if let queue { + queue.async(execute: block) + } else { + block() + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelStore.swift b/ios/MullvadVPN/TunnelManager/TunnelStore.swift index 07266bcf170d..b93c33ac45c2 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelStore.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelStore.swift @@ -11,16 +11,23 @@ import MullvadLogging import NetworkExtension import UIKit +protocol TunnelStoreProtocol { + associatedtype TunnelType: TunnelProtocol, Equatable + func getPersistentTunnels() -> [TunnelType] + func createNewTunnel() -> TunnelType +} + /// Wrapper around system VPN tunnels. -final class TunnelStore: TunnelStatusObserver { +final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver { + typealias TunnelType = Tunnel private let logger = Logger(label: "TunnelStore") private let lock = NSLock() /// Persistent tunnels registered with the system. - private var persistentTunnels: [Tunnel] = [] + private var persistentTunnels: [TunnelType] = [] /// Newly created tunnels, stored as collection of weak boxes. - private var newTunnels: [WeakBox] = [] + private var newTunnels: [WeakBox] = [] init(application: UIApplication) { NotificationCenter.default.addObserver( @@ -31,7 +38,7 @@ final class TunnelStore: TunnelStatusObserver { ) } - func getPersistentTunnels() -> [Tunnel] { + func getPersistentTunnels() -> [TunnelType] { lock.lock() defer { lock.unlock() } @@ -66,12 +73,12 @@ final class TunnelStore: TunnelStatusObserver { } } - func createNewTunnel() -> Tunnel { + func createNewTunnel() -> TunnelType { lock.lock() defer { lock.unlock() } let tunnelProviderManager = TunnelProviderManagerType() - let tunnel = Tunnel(tunnelProvider: tunnelProviderManager) + let tunnel = TunnelType(tunnelProvider: tunnelProviderManager) tunnel.addObserver(self) newTunnels = newTunnels.filter { $0.value != nil } @@ -82,20 +89,23 @@ final class TunnelStore: TunnelStatusObserver { return tunnel } - func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) { + func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) { lock.lock() defer { lock.unlock() } - handleTunnelStatus(tunnel: tunnel, status: status) + // swiftlint:disable:next force_cast + handleTunnelStatus(tunnel: tunnel as! TunnelType, status: status) } - private func handleTunnelStatus(tunnel: Tunnel, status: NEVPNStatus) { - if status == .invalid, let index = persistentTunnels.firstIndex(of: tunnel) { + private func handleTunnelStatus(tunnel: TunnelType, status: NEVPNStatus) { + if status == .invalid, + let index = persistentTunnels.firstIndex(of: tunnel) { persistentTunnels.remove(at: index) logger.debug("Persistent tunnel was removed: \(tunnel.logFormat()).") } - if status != .invalid, let index = newTunnels.firstIndex(where: { $0.value == tunnel }) { + if status != .invalid, + let index = newTunnels.compactMap({ $0.value }).firstIndex(where: { $0 == tunnel }) { newTunnels.remove(at: index) persistentTunnels.append(tunnel) logger.debug("New tunnel became persistent: \(tunnel.logFormat()).") diff --git a/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift b/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift new file mode 100644 index 000000000000..046244582c75 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift @@ -0,0 +1,25 @@ +// +// UIApplication+Extensions.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-10-02. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +#if canImport(UIKit) + +import Foundation +import UIKit + +public protocol BackgroundTaskProvider { + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) + + func beginBackgroundTask( + withName taskName: String?, + expirationHandler handler: (() -> Void)? + ) -> UIBackgroundTaskIdentifier +} + +extension UIApplication: BackgroundTaskProvider {} + +#endif diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift index 2b880ab1c915..55f7e0eb8814 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift @@ -16,13 +16,13 @@ import Operations class UpdateAccountDataOperation: ResultOperation { private let logger = Logger(label: "UpdateAccountDataOperation") private let interactor: TunnelInteractor - private let accountsProxy: REST.AccountsProxy + private let accountsProxy: RESTAccountHandling private var task: Cancellable? init( dispatchQueue: DispatchQueue, interactor: TunnelInteractor, - accountsProxy: REST.AccountsProxy + accountsProxy: RESTAccountHandling ) { self.interactor = interactor self.accountsProxy = accountsProxy diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift index 85700d8dca01..6803dd7d87c4 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift @@ -16,14 +16,14 @@ import class WireGuardKitTypes.PublicKey class UpdateDeviceDataOperation: ResultOperation { private let interactor: TunnelInteractor - private let devicesProxy: REST.DevicesProxy + private let devicesProxy: DeviceHandling private var task: Cancellable? init( dispatchQueue: DispatchQueue, interactor: TunnelInteractor, - devicesProxy: REST.DevicesProxy + devicesProxy: DeviceHandling ) { self.interactor = interactor self.devicesProxy = devicesProxy diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index a08bd10064e1..05d2d4046507 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -16,7 +16,7 @@ import StoreKit final class AccountInteractor { private let storePaymentManager: StorePaymentManager let tunnelManager: TunnelManager - let accountsProxy: REST.AccountsProxy + let accountsProxy: RESTAccountHandling var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)? var didReceiveDeviceState: ((DeviceState) -> Void)? @@ -27,7 +27,7 @@ final class AccountInteractor { init( storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager, - accountsProxy: REST.AccountsProxy + accountsProxy: RESTAccountHandling ) { self.storePaymentManager = storePaymentManager self.tunnelManager = tunnelManager diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift index 5f9065a00696..581b5e0d0a11 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift @@ -12,10 +12,10 @@ import MullvadTypes import Operations class DeviceManagementInteractor { - private let devicesProxy: REST.DevicesProxy + private let devicesProxy: DeviceHandling private let accountNumber: String - init(accountNumber: String, devicesProxy: REST.DevicesProxy) { + init(accountNumber: String, devicesProxy: DeviceHandling) { self.accountNumber = accountNumber self.devicesProxy = devicesProxy } diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift index b1bb49ccb2b8..9dbf3a5b6b3f 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift @@ -12,7 +12,7 @@ import MullvadTypes import Operations final class ProblemReportInteractor { - private let apiProxy: REST.APIProxy + private let apiProxy: APIQuerying private let tunnelManager: TunnelManager private lazy var consolidatedLog: ConsolidatedApplicationLog = { @@ -31,7 +31,7 @@ final class ProblemReportInteractor { return report }() - init(apiProxy: REST.APIProxy, tunnelManager: TunnelManager) { + init(apiProxy: APIQuerying, tunnelManager: TunnelManager) { self.apiProxy = apiProxy self.tunnelManager = tunnelManager } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift index fa7639122718..26da26f14723 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift @@ -12,7 +12,7 @@ import MullvadTypes final class RedeemVoucherInteractor { private let tunnelManager: TunnelManager - private let accountsProxy: REST.AccountsProxy + private let accountsProxy: RESTAccountHandling private let shouldVerifyVoucherAsAccount: Bool private var tasks: [Cancellable] = [] @@ -23,7 +23,7 @@ final class RedeemVoucherInteractor { init( tunnelManager: TunnelManager, - accountsProxy: REST.AccountsProxy, + accountsProxy: RESTAccountHandling, verifyVoucherAsAccount: Bool ) { self.tunnelManager = tunnelManager diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift index 2af17ec14748..3454516808fc 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift @@ -13,13 +13,13 @@ import RelayCache final class SettingsInteractorFactory { private let storePaymentManager: StorePaymentManager private let tunnelManager: TunnelManager - private let apiProxy: REST.APIProxy + private let apiProxy: APIQuerying private let relayCacheTracker: RelayCacheTracker init( storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager, - apiProxy: REST.APIProxy, + apiProxy: APIQuerying, relayCacheTracker: RelayCacheTracker ) { self.storePaymentManager = storePaymentManager diff --git a/ios/MullvadVPNTests/APIProxy+Stubs.swift b/ios/MullvadVPNTests/APIProxy+Stubs.swift new file mode 100644 index 000000000000..7602a1fe7aa2 --- /dev/null +++ b/ios/MullvadVPNTests/APIProxy+Stubs.swift @@ -0,0 +1,64 @@ +// +// APIProxy+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadREST +@testable import MullvadTypes +@testable import WireGuardKitTypes + +struct APIProxyStub: APIQuerying { + func getAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable { + AnyCancellable() + } + + func getRelays( + etag: String?, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func createApplePayment( + accountNumber: String, + receiptString: Data + ) -> any RESTRequestExecutor { + RESTRequestExecutorStub(success: { + .timeAdded(42, .distantFuture) + }) + } + + func createApplePayment( + accountNumber: String, + receiptString: Data, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func sendProblemReport( + _ body: REST.ProblemReportRequest, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func submitVoucher( + voucherCode: String, + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } +} diff --git a/ios/MullvadVPNTests/AccessTokenManager+Stubs.swift b/ios/MullvadVPNTests/AccessTokenManager+Stubs.swift new file mode 100644 index 000000000000..a36719314efa --- /dev/null +++ b/ios/MullvadVPNTests/AccessTokenManager+Stubs.swift @@ -0,0 +1,22 @@ +// +// AccessTokenManager+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadREST +@testable import MullvadTypes + +struct AccessTokenManagerStub: RESTAccessTokenManagement { + func getAccessToken( + accountNumber: String, + completionHandler: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func invalidateAllTokens() {} +} diff --git a/ios/MullvadVPNTests/AccountsProxy+Stubs.swift b/ios/MullvadVPNTests/AccountsProxy+Stubs.swift new file mode 100644 index 000000000000..6dc2afdade60 --- /dev/null +++ b/ios/MullvadVPNTests/AccountsProxy+Stubs.swift @@ -0,0 +1,42 @@ +// +// AccountsProxy+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadREST +@testable import MullvadTypes + +struct AccountsProxyStub: RESTAccountHandling { + func createAccount( + retryStrategy: REST.RetryStrategy, + completion: @escaping MullvadREST.ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func getAccountData(accountNumber: String) -> any RESTRequestExecutor { + RESTRequestExecutorStub(success: { + Account(id: accountNumber, expiry: .distantFuture, maxDevices: 1, canAddDevices: true) + }) + } + + func getAccountData( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func deleteAccount( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } +} diff --git a/ios/MullvadVPNTests/AddressCacheTests.swift b/ios/MullvadVPNTests/AddressCacheTests.swift index 5a5dedfe1acb..a42aa3b8dde3 100644 --- a/ios/MullvadVPNTests/AddressCacheTests.swift +++ b/ios/MullvadVPNTests/AddressCacheTests.swift @@ -42,10 +42,14 @@ final class AddressCacheTests: XCTestCase { addressCache.setEndpoints([apiEndpoint]) let dateBeforeUpdate = addressCache.getLastUpdateDate() + // Calling `Date()` several times in a row can result in the same Date object being returned. + // Force a sleep before setting the next endpoint to avoid getting the same Date object twice in a row. + Thread.sleep(forTimeInterval: Duration.milliseconds(10).timeInterval) addressCache.setEndpoints([apiEndpoint]) let dateAfterUpdate = addressCache.getLastUpdateDate() - XCTAssertNotEqual(dateBeforeUpdate, dateAfterUpdate) + let timeDifference = dateAfterUpdate.timeIntervalSince(dateBeforeUpdate) + XCTAssertNotEqual(0.0, timeDifference) } func testSetEndpointsDoesNotDoAnythingIfSettingEmptyEndpoints() throws { diff --git a/ios/MullvadVPNTests/DevicesProxy+Stubs.swift b/ios/MullvadVPNTests/DevicesProxy+Stubs.swift new file mode 100644 index 000000000000..3ec0f3faff59 --- /dev/null +++ b/ios/MullvadVPNTests/DevicesProxy+Stubs.swift @@ -0,0 +1,59 @@ +// +// DevicesProxy+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadREST +@testable import MullvadTypes +@testable import WireGuardKitTypes + +struct DevicesProxyStub: DeviceHandling { + func getDevice( + accountNumber: String, + identifier: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func getDevices( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<[Device]> + ) -> Cancellable { + AnyCancellable() + } + + func createDevice( + accountNumber: String, + request: REST.CreateDeviceRequest, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func deleteDevice( + accountNumber: String, + identifier: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } + + func rotateDeviceKey( + accountNumber: String, + identifier: String, + publicKey: PublicKey, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler + ) -> Cancellable { + AnyCancellable() + } +} diff --git a/ios/MullvadVPNTests/RESTRequestExecutor+Stubs.swift b/ios/MullvadVPNTests/RESTRequestExecutor+Stubs.swift new file mode 100644 index 000000000000..cd7fad6c76aa --- /dev/null +++ b/ios/MullvadVPNTests/RESTRequestExecutor+Stubs.swift @@ -0,0 +1,38 @@ +// +// RESTRequestExecutor+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadREST +@testable import MullvadTypes + +struct RESTRequestExecutorStub: RESTRequestExecutor { + typealias Success = Success + + var success: (() -> Success)? + + func execute(completionHandler: @escaping (Result) -> Void) -> Cancellable { + AnyCancellable() + } + + func execute( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping (Result) -> Void + ) -> Cancellable { + AnyCancellable() + } + + func execute() async throws -> Success { + try await execute(retryStrategy: .noRetry) + } + + func execute(retryStrategy: REST.RetryStrategy) async throws -> Success { + guard let success = success else { throw POSIXError(.EINVAL) } + + return success() + } +} diff --git a/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift b/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift new file mode 100644 index 000000000000..ef6fcd9ab6b2 --- /dev/null +++ b/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift @@ -0,0 +1,33 @@ +// +// RelayCacheTracker+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadTypes +@testable import RelayCache + +struct RelayCacheTrackerStub: RelayCacheTrackerProtocol { + func startPeriodicUpdates() {} + + func stopPeriodicUpdates() {} + + func updateRelays(completionHandler: ((Result) -> Void)?) -> Cancellable { + AnyCancellable() + } + + func getCachedRelays() throws -> CachedRelays { + CachedRelays(relays: ServerRelaysResponseStubs.sampleRelays, updatedAt: Date()) + } + + func getNextUpdateDate() -> Date { + Date() + } + + func addObserver(_ observer: RelayCacheTrackerObserver) {} + + func removeObserver(_ observer: RelayCacheTrackerObserver) {} +} diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift index fa8fc6659bcb..b390592b365a 100644 --- a/ios/MullvadVPNTests/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/RelaySelectorTests.swift @@ -16,6 +16,8 @@ private let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]] private let defaultPort: UInt16 = 53 class RelaySelectorTests: XCTestCase { + let sampleRelays = ServerRelaysResponseStubs.sampleRelays + func testCountryConstraint() throws { let constraints = RelayConstraints(location: .only(.country("es"))) @@ -106,199 +108,3 @@ class RelaySelectorTests: XCTestCase { XCTAssertTrue(sampleRelays.bridge.relays.contains(selectedRelay)) } } - -private let sampleRelays = REST.ServerRelaysResponse( - locations: [ - "es-mad": REST.ServerLocation( - country: "Spain", - city: "Madrid", - latitude: 40.408566, - longitude: -3.69222 - ), - "se-got": REST.ServerLocation( - country: "Sweden", - city: "Gothenburg", - latitude: 57.70887, - longitude: 11.97456 - ), - "se-sto": REST.ServerLocation( - country: "Sweden", - city: "Stockholm", - latitude: 59.3289, - longitude: 18.0649 - ), - "ae-dxb": REST.ServerLocation( - country: "United Arab Emirates", - city: "Dubai", - latitude: 25.276987, - longitude: 55.296249 - ), - "jp-tyo": REST.ServerLocation( - country: "Japan", - city: "Tokyo", - latitude: 35.685, - longitude: 139.751389 - ), - "ca-tor": REST.ServerLocation( - country: "Canada", - city: "Toronto", - latitude: 43.666667, - longitude: -79.416667 - ), - "us-atl": REST.ServerLocation( - country: "USA", - city: "Atlanta, GA", - latitude: 40.73061, - longitude: -73.935242 - ), - "us-dal": REST.ServerLocation( - country: "USA", - city: "Dallas, TX", - latitude: 32.89748, - longitude: -97.040443 - ), - ], - wireguard: REST.ServerWireguardTunnels( - ipv4Gateway: .loopback, - ipv6Gateway: .loopback, - portRanges: portRanges, - relays: [ - REST.ServerRelay( - hostname: "es1-wireguard", - active: true, - owned: true, - location: "es-mad", - provider: "", - weight: 500, - ipv4AddrIn: .loopback, - ipv6AddrIn: .loopback, - publicKey: Data(), - includeInCountry: true - ), - REST.ServerRelay( - hostname: "se10-wireguard", - active: true, - owned: true, - location: "se-got", - provider: "", - weight: 1000, - ipv4AddrIn: .loopback, - ipv6AddrIn: .loopback, - publicKey: Data(), - includeInCountry: true - ), - REST.ServerRelay( - hostname: "se2-wireguard", - active: true, - owned: true, - location: "se-sto", - provider: "", - weight: 50, - ipv4AddrIn: .loopback, - ipv6AddrIn: .loopback, - publicKey: Data(), - includeInCountry: true - ), - REST.ServerRelay( - hostname: "se6-wireguard", - active: true, - owned: true, - location: "se-sto", - provider: "", - weight: 100, - ipv4AddrIn: .loopback, - ipv6AddrIn: .loopback, - publicKey: Data(), - includeInCountry: true - ), - REST.ServerRelay( - hostname: "us-dal-wg-001", - active: true, - owned: true, - location: "us-dal", - provider: "", - weight: 100, - ipv4AddrIn: .loopback, - ipv6AddrIn: .loopback, - publicKey: Data(), - includeInCountry: true - ), - REST.ServerRelay( - hostname: "us-nyc-wg-301", - active: true, - owned: true, - location: "us-nyc", - provider: "", - weight: 100, - ipv4AddrIn: .loopback, - ipv6AddrIn: .loopback, - publicKey: Data(), - includeInCountry: true - ), - ] - ), - bridge: REST.ServerBridges(shadowsocks: [ - REST.ServerShadowsocks(protocol: "tcp", port: 443, cipher: "aes-256-gcm", password: "mullvad"), - ], relays: [ - REST.BridgeRelay( - hostname: "se-sto-br-001", - active: true, - owned: true, - location: "se-sto", - provider: "31173", - ipv4AddrIn: .loopback, - weight: 100, - includeInCountry: true - ), - REST.BridgeRelay( - hostname: "jp-tyo-br-101", - active: true, - owned: true, - location: "jp-tyo", - provider: "M247", - ipv4AddrIn: .loopback, - weight: 1, - includeInCountry: true - ), - REST.BridgeRelay( - hostname: "ca-tor-ovpn-001", - active: false, - owned: false, - location: "ca-tor", - provider: "M247", - ipv4AddrIn: .loopback, - weight: 1, - includeInCountry: true - ), - REST.BridgeRelay( - hostname: "ae-dxb-ovpn-001", - active: true, - owned: false, - location: "ae-dxb", - provider: "M247", - ipv4AddrIn: .loopback, - weight: 100, - includeInCountry: true - ), - REST.BridgeRelay( - hostname: "us-atl-br-101", - active: true, - owned: false, - location: "us-atl", - provider: "100TB", - ipv4AddrIn: .loopback, - weight: 100, - includeInCountry: true - ), - REST.BridgeRelay( - hostname: "us-dal-br-101", - active: true, - owned: false, - location: "us-dal", - provider: "100TB", - ipv4AddrIn: .loopback, - weight: 100, - includeInCountry: true - ), - ]) -) diff --git a/ios/MullvadVPNTests/ServerRelaysResponse+Stubs.swift b/ios/MullvadVPNTests/ServerRelaysResponse+Stubs.swift new file mode 100644 index 000000000000..8bca08f4f8eb --- /dev/null +++ b/ios/MullvadVPNTests/ServerRelaysResponse+Stubs.swift @@ -0,0 +1,211 @@ +// +// ServerRelaysResponse+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadREST +import WireGuardKitTypes + +enum ServerRelaysResponseStubs { + static let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]] + + static let sampleRelays = REST.ServerRelaysResponse( + locations: [ + "es-mad": REST.ServerLocation( + country: "Spain", + city: "Madrid", + latitude: 40.408566, + longitude: -3.69222 + ), + "se-got": REST.ServerLocation( + country: "Sweden", + city: "Gothenburg", + latitude: 57.70887, + longitude: 11.97456 + ), + "se-sto": REST.ServerLocation( + country: "Sweden", + city: "Stockholm", + latitude: 59.3289, + longitude: 18.0649 + ), + "ae-dxb": REST.ServerLocation( + country: "United Arab Emirates", + city: "Dubai", + latitude: 25.276987, + longitude: 55.296249 + ), + "jp-tyo": REST.ServerLocation( + country: "Japan", + city: "Tokyo", + latitude: 35.685, + longitude: 139.751389 + ), + "ca-tor": REST.ServerLocation( + country: "Canada", + city: "Toronto", + latitude: 43.666667, + longitude: -79.416667 + ), + "us-atl": REST.ServerLocation( + country: "USA", + city: "Atlanta, GA", + latitude: 40.73061, + longitude: -73.935242 + ), + "us-dal": REST.ServerLocation( + country: "USA", + city: "Dallas, TX", + latitude: 32.89748, + longitude: -97.040443 + ), + ], + wireguard: REST.ServerWireguardTunnels( + ipv4Gateway: .loopback, + ipv6Gateway: .loopback, + portRanges: portRanges, + relays: [ + REST.ServerRelay( + hostname: "es1-wireguard", + active: true, + owned: true, + location: "es-mad", + provider: "", + weight: 500, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "se10-wireguard", + active: true, + owned: true, + location: "se-got", + provider: "", + weight: 1000, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "se2-wireguard", + active: true, + owned: true, + location: "se-sto", + provider: "", + weight: 50, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "se6-wireguard", + active: true, + owned: true, + location: "se-sto", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "us-dal-wg-001", + active: true, + owned: true, + location: "us-dal", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + REST.ServerRelay( + hostname: "us-nyc-wg-301", + active: true, + owned: true, + location: "us-nyc", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: PrivateKey().publicKey.rawValue, + includeInCountry: true + ), + ] + ), + bridge: REST.ServerBridges(shadowsocks: [ + REST.ServerShadowsocks(protocol: "tcp", port: 443, cipher: "aes-256-gcm", password: "mullvad"), + ], relays: [ + REST.BridgeRelay( + hostname: "se-sto-br-001", + active: true, + owned: true, + location: "se-sto", + provider: "31173", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "jp-tyo-br-101", + active: true, + owned: true, + location: "jp-tyo", + provider: "M247", + ipv4AddrIn: .loopback, + weight: 1, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "ca-tor-ovpn-001", + active: false, + owned: false, + location: "ca-tor", + provider: "M247", + ipv4AddrIn: .loopback, + weight: 1, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "ae-dxb-ovpn-001", + active: true, + owned: false, + location: "ae-dxb", + provider: "M247", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "us-atl-br-101", + active: true, + owned: false, + location: "us-atl", + provider: "100TB", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + REST.BridgeRelay( + hostname: "us-dal-br-101", + active: true, + owned: false, + location: "us-dal", + provider: "100TB", + ipv4AddrIn: .loopback, + weight: 100, + includeInCountry: true + ), + ]) + ) +} diff --git a/ios/MullvadVPNTests/TunnelManagerTests.swift b/ios/MullvadVPNTests/TunnelManagerTests.swift new file mode 100644 index 000000000000..958c50e25275 --- /dev/null +++ b/ios/MullvadVPNTests/TunnelManagerTests.swift @@ -0,0 +1,33 @@ +// +// TunnelManagerTests.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-02. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +final class TunnelManagerTests: XCTestCase { + func testTunnelManager() { + let application = UIApplicationStub() + let tunnelStore = TunnelStoreStub() + let relayCacheTracker = RelayCacheTrackerStub() + let accountProxy = AccountsProxyStub() + let devicesProxy = DevicesProxyStub() + let apiProxy = APIProxyStub() + let accessTokenManager = AccessTokenManagerStub() + + let tunnelManager = TunnelManager( + application: application, + tunnelStore: tunnelStore, + relayCacheTracker: relayCacheTracker, + accountsProxy: accountProxy, + devicesProxy: devicesProxy, + apiProxy: apiProxy, + accessTokenManager: accessTokenManager + ) + + XCTAssertNotNil(tunnelManager) + } +} diff --git a/ios/MullvadVPNTests/TunnelStore+Stubs.swift b/ios/MullvadVPNTests/TunnelStore+Stubs.swift new file mode 100644 index 000000000000..546583942628 --- /dev/null +++ b/ios/MullvadVPNTests/TunnelStore+Stubs.swift @@ -0,0 +1,74 @@ +// +// TunnelStore+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-03. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension + +struct TunnelStoreStub: TunnelStoreProtocol { + typealias TunnelType = TunnelStub + func getPersistentTunnels() -> [TunnelType] { + [] + } + + func createNewTunnel() -> TunnelType { + TunnelStub(status: .invalid, isOnDemandEnabled: false) + } +} + +class DummyTunnelStatusObserver: TunnelStatusObserver { + func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) {} +} + +final class TunnelStub: TunnelProtocol, Equatable { + convenience init(tunnelProvider: TunnelProviderManagerType) { + self.init(status: .invalid, isOnDemandEnabled: false) + } + + static func == (lhs: TunnelStub, rhs: TunnelStub) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } + + init(status: NEVPNStatus, isOnDemandEnabled: Bool, startDate: Date? = nil) { + self.status = status + self.isOnDemandEnabled = isOnDemandEnabled + self.startDate = startDate + } + + func addObserver(_ observer: TunnelStatusObserver) {} + + func removeObserver(_ observer: TunnelStatusObserver) {} + + var status: NEVPNStatus + + var isOnDemandEnabled: Bool + + var startDate: Date? + + func addBlockObserver( + queue: DispatchQueue?, + handler: @escaping (any TunnelProtocol, NEVPNStatus) -> Void + ) -> TunnelStatusBlockObserver { + TunnelStatusBlockObserver(tunnel: self, queue: queue, handler: handler) + } + + func logFormat() -> String { + "" + } + + func saveToPreferences(_ completion: @escaping (Error?) -> Void) {} + + func removeFromPreferences(completion: @escaping (Error?) -> Void) {} + + func setConfiguration(_ configuration: TunnelConfiguration) {} + + func start(options: [String: NSObject]?) throws {} + + func stop() {} + + func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {} +} diff --git a/ios/MullvadVPNTests/UIApplication+Stubs.swift b/ios/MullvadVPNTests/UIApplication+Stubs.swift new file mode 100644 index 000000000000..8b8fbe4999ca --- /dev/null +++ b/ios/MullvadVPNTests/UIApplication+Stubs.swift @@ -0,0 +1,23 @@ +// +// UIApplication+Stubs.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-10-02. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +@testable import MullvadTypes + +struct UIApplicationStub: BackgroundTaskProvider { + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) {} + + func beginBackgroundTask( + withName taskName: String?, + expirationHandler handler: (() -> Void)? + ) -> UIBackgroundTaskIdentifier { + .invalid + } +} diff --git a/ios/Operations/BackgroundObserver.swift b/ios/Operations/BackgroundObserver.swift index 53889cecd99b..0dcbcb3287f2 100644 --- a/ios/Operations/BackgroundObserver.swift +++ b/ios/Operations/BackgroundObserver.swift @@ -8,17 +8,18 @@ #if canImport(UIKit) +import MullvadTypes import UIKit @available(iOSApplicationExtension, unavailable) public final class BackgroundObserver: OperationObserver { public let name: String - public let application: UIApplication + public let application: BackgroundTaskProvider public let cancelUponExpiration: Bool private var taskIdentifier: UIBackgroundTaskIdentifier? - public init(application: UIApplication, name: String, cancelUponExpiration: Bool) { + public init(application: BackgroundTaskProvider, name: String, cancelUponExpiration: Bool) { self.application = application self.name = name self.cancelUponExpiration = cancelUponExpiration diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift index 9ad8bcbc75bf..bbefc302158a 100644 --- a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift +++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift @@ -13,10 +13,10 @@ import class WireGuardKitTypes.PublicKey /// An object that implements remote service used by `DeviceCheckOperation`. struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol { - private let accountsProxy: REST.AccountsProxy - private let devicesProxy: REST.DevicesProxy + private let accountsProxy: RESTAccountHandling + private let devicesProxy: DeviceHandling - init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) { + init(accountsProxy: RESTAccountHandling, devicesProxy: DeviceHandling) { self.accountsProxy = accountsProxy self.devicesProxy = devicesProxy } diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift index f7d22d16db2f..decf72b1bc98 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift @@ -57,6 +57,10 @@ public struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol { // packet tunnel provider. return .tunnelAdapter + case is PublicKeyError: + // Returned when there is an endpoint but its public key is invalid. + return .invalidRelayPublicKey + default: // Everything else in case we introduce new errors and forget to handle them. return .unknown diff --git a/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift b/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift index e4fa756b25c0..a1933510bdf2 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/DeviceChecker.swift @@ -16,10 +16,10 @@ final class DeviceChecker { private let dispatchQueue = DispatchQueue(label: "DeviceCheckerQueue") private let operationQueue = AsyncOperationQueue.makeSerial() - private let accountsProxy: REST.AccountsProxy - private let devicesProxy: REST.DevicesProxy + private let accountsProxy: RESTAccountHandling + private let devicesProxy: DeviceHandling - init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) { + init(accountsProxy: RESTAccountHandling, devicesProxy: DeviceHandling) { self.accountsProxy = accountsProxy self.devicesProxy = devicesProxy } diff --git a/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift b/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift deleted file mode 100644 index 99862e152199..000000000000 --- a/ios/PacketTunnel/PacketTunnelProvider/State+Extensions.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// State+Extensions.swift -// PacketTunnel -// -// Created by pronebird on 12/09/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadTypes -import PacketTunnelCore - -extension State { - var packetTunnelStatus: PacketTunnelStatus { - var status = PacketTunnelStatus() - - switch self { - case let .connecting(connState), - let .connected(connState), - let .reconnecting(connState), - let .disconnecting(connState): - switch connState.networkReachability { - case .reachable: - status.isNetworkReachable = true - case .unreachable: - status.isNetworkReachable = false - case .undetermined: - // TODO: fix me - status.isNetworkReachable = true - } - - status.numberOfFailedAttempts = connState.connectionAttemptCount - status.tunnelRelay = connState.selectedRelay.packetTunnelRelay - - case .disconnected, .initial: - break - - case let .error(blockedState): - status.blockedStateReason = blockedState.reason - } - - return status - } - - var relayConstraints: RelayConstraints? { - switch self { - case let .connecting(connState), let .connected(connState), let .reconnecting(connState): - return connState.relayConstraints - - case let .error(blockedState): - return blockedState.relayConstraints - - case .initial, .disconnecting, .disconnected: - return nil - } - } -} diff --git a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift index c4a731aa7829..6ba03db89b3e 100644 --- a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift +++ b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift @@ -13,6 +13,15 @@ import struct WireGuardKitTypes.IPAddressRange import class WireGuardKitTypes.PrivateKey import class WireGuardKitTypes.PublicKey +/// Error returned when there is an endpoint but its public key is invalid. +public struct PublicKeyError: LocalizedError { + let endpoint: MullvadEndpoint + + public var errorDescription: String? { + "Public key is invalid, endpoint: \(endpoint)" + } +} + /// Struct building tunnel adapter configuration. struct ConfigurationBuilder { var privateKey: PrivateKey @@ -20,22 +29,28 @@ struct ConfigurationBuilder { var dns: SelectedDNSServers? var endpoint: MullvadEndpoint? - func makeConfiguration() -> TunnelAdapterConfiguration { + func makeConfiguration() throws -> TunnelAdapterConfiguration { return TunnelAdapterConfiguration( privateKey: privateKey, interfaceAddresses: interfaceAddresses, dns: dnsServers, - peer: peer + peer: try peer ) } private var peer: TunnelPeer? { - guard let endpoint else { return nil } + get throws { + guard let endpoint else { return nil } - return TunnelPeer( - endpoint: .ipv4(endpoint.ipv4Relay), - publicKey: PublicKey(rawValue: endpoint.publicKey)! - ) + guard let publicKey = PublicKey(rawValue: endpoint.publicKey) else { + throw PublicKeyError(endpoint: endpoint) + } + + return TunnelPeer( + endpoint: .ipv4(endpoint.ipv4Relay), + publicKey: publicKey + ) + } } private var dnsServers: [IPAddress] { diff --git a/ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+ErrorState.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+ErrorState.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+Extensions.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+Extensions.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift similarity index 98% rename from ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift index 0cf8351523c2..5ab13353e915 100644 --- a/ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+KeyPolicy.swift @@ -159,7 +159,7 @@ extension PacketTunnelActor { /** Internal helper that transitions key policy from `.usePrior` to `.useCurrent`. - - Parameter keyPolicy: a reference to key policy hend either in connection state or blocked state struct. + - Parameter keyPolicy: a reference to key policy held either in connection state or blocked state struct. - Returns: `true` when the policy was modified, otherwise `false`. */ private func setCurrentKeyPolicy(_ keyPolicy: inout KeyPolicy) -> Bool { diff --git a/ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+NetworkReachability.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+NetworkReachability.swift diff --git a/ios/PacketTunnelCore/Actor/Actor+Public.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift similarity index 97% rename from ios/PacketTunnelCore/Actor/Actor+Public.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift index 7a50477c29d5..b54d397ca47d 100644 --- a/ios/PacketTunnelCore/Actor/Actor+Public.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift @@ -1,5 +1,5 @@ // -// Actor+Public.swift +// PacketTunnelActor+Public.swift // PacketTunnelCore // // Created by pronebird on 27/09/2023. diff --git a/ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+SleepCycle.swift similarity index 100% rename from ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor+SleepCycle.swift diff --git a/ios/PacketTunnelCore/Actor/Actor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift similarity index 99% rename from ios/PacketTunnelCore/Actor/Actor.swift rename to ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 546ae3f59e72..c82b0f8ccbde 100644 --- a/ios/PacketTunnelCore/Actor/Actor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -1,5 +1,5 @@ // -// Actor.swift +// PacketTunnelActor.swift // PacketTunnel // // Created by pronebird on 30/06/2023. @@ -378,3 +378,5 @@ extension PacketTunnelActor { } } } + +extension PacketTunnelActor: PacketTunnelActorProtocol {} diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift new file mode 100644 index 000000000000..d5acf6bae390 --- /dev/null +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift @@ -0,0 +1,16 @@ +// +// PacketTunnelActorProtocol.swift +// PacketTunnelCoreTests +// +// Created by Jon Petersson on 2023-10-11. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public protocol PacketTunnelActorProtocol { + var state: State { get async } + + func reconnect(to nextRelay: NextRelay) + func notifyKeyRotation(date: Date?) +} diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift index 71a090acb268..6e6d40bfb760 100644 --- a/ios/PacketTunnelCore/Actor/State+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift @@ -1,5 +1,5 @@ // -// State+.swift +// State+Extensions.swift // PacketTunnelCore // // Created by pronebird on 08/09/2023. @@ -7,6 +7,7 @@ // import Foundation +import MullvadTypes import class WireGuardKitTypes.PrivateKey extension State { @@ -36,6 +37,50 @@ extension State { } } + var packetTunnelStatus: PacketTunnelStatus { + var status = PacketTunnelStatus() + + switch self { + case let .connecting(connState), + let .connected(connState), + let .reconnecting(connState), + let .disconnecting(connState): + switch connState.networkReachability { + case .reachable: + status.isNetworkReachable = true + case .unreachable: + status.isNetworkReachable = false + case .undetermined: + // TODO: fix me + status.isNetworkReachable = true + } + + status.numberOfFailedAttempts = connState.connectionAttemptCount + status.tunnelRelay = connState.selectedRelay.packetTunnelRelay + + case .disconnected, .initial: + break + + case let .error(blockedState): + status.blockedStateReason = blockedState.reason + } + + return status + } + + public var relayConstraints: RelayConstraints? { + switch self { + case let .connecting(connState), let .connected(connState), let .reconnecting(connState): + return connState.relayConstraints + + case let .error(blockedState): + return blockedState.relayConstraints + + case .initial, .disconnecting, .disconnected: + return nil + } + } + // MARK: - Logging func logFormat() -> String { @@ -105,7 +150,7 @@ extension BlockedStateReason { return true case .noRelaysSatisfyingConstraints, .readSettings, .invalidAccount, .deviceRevoked, .tunnelAdapter, .unknown, - .deviceLoggedOut, .outdatedSchema: + .deviceLoggedOut, .outdatedSchema, .invalidRelayPublicKey: return false } } diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 36a427e4964c..eba9f1baa832 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -1,5 +1,5 @@ // -// States.swift +// State.swift // PacketTunnel // // Created by pronebird on 07/08/2023. @@ -105,7 +105,7 @@ public struct ConnectionState { /// This is primarily used by packet tunnel for updating constraints in tunnel provider. public var relayConstraints: RelayConstraints - /// Last WG key read from setings. + /// Last WG key read from settings. /// Can be `nil` if moved to `keyPolicy`. public var currentKey: PrivateKey? @@ -187,6 +187,9 @@ public enum BlockedStateReason: String, Codable, Equatable { /// Tunnel adapter error. case tunnelAdapter + /// Invalid public key. + case invalidRelayPublicKey + /// Unidentified reason. case unknown } diff --git a/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift b/ios/PacketTunnelCore/IPC/AppMessageHandler.swift similarity index 87% rename from ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift rename to ios/PacketTunnelCore/IPC/AppMessageHandler.swift index 4071468799bc..d72e2bf05b0a 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/AppMessageHandler.swift +++ b/ios/PacketTunnelCore/IPC/AppMessageHandler.swift @@ -8,17 +8,16 @@ import Foundation import MullvadLogging -import PacketTunnelCore /** Actor handling packet tunnel IPC (app) messages and patching them through to the right facility. */ -struct AppMessageHandler { +public struct AppMessageHandler { private let logger = Logger(label: "AppMessageHandler") - private let packetTunnelActor: PacketTunnelActor - private let urlRequestProxy: URLRequestProxy + private let packetTunnelActor: PacketTunnelActorProtocol + private let urlRequestProxy: URLRequestProxyProtocol - init(packetTunnelActor: PacketTunnelActor, urlRequestProxy: URLRequestProxy) { + public init(packetTunnelActor: PacketTunnelActorProtocol, urlRequestProxy: URLRequestProxyProtocol) { self.packetTunnelActor = packetTunnelActor self.urlRequestProxy = urlRequestProxy } @@ -34,7 +33,7 @@ struct AppMessageHandler { the acknowledgment from IPC before starting next operation, hence it's critical to return as soon as possible. (See `TunnelManager.reconnectTunnel()`, `SendTunnelProviderMessageOperation`) */ - func handleAppMessage(_ messageData: Data) async -> Data? { + public func handleAppMessage(_ messageData: Data) async -> Data? { guard let message = decodeMessage(messageData) else { return nil } logger.debug("Received app message: \(message)") @@ -51,7 +50,7 @@ struct AppMessageHandler { return await encodeReply(packetTunnelActor.state.packetTunnelStatus) case .privateKeyRotation: - packetTunnelActor.notifyKeyRotation(date: nil) + packetTunnelActor.notifyKeyRotation(date: Date()) return nil case let .reconnectTunnel(nextRelay): diff --git a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift index fb4f6cbbfc02..11d7f77d2e17 100644 --- a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift +++ b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift @@ -86,3 +86,5 @@ extension URLRequestProxy { } } } + +extension URLRequestProxy: URLRequestProxyProtocol {} diff --git a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift new file mode 100644 index 000000000000..da52b447ba8e --- /dev/null +++ b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift @@ -0,0 +1,15 @@ +// +// URLRequestProxyProtocol.swift +// PacketTunnelCore +// +// Created by Jon Petersson on 2023-10-11. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public protocol URLRequestProxyProtocol { + func sendRequest(_ proxyRequest: ProxyURLRequest, completionHandler: @escaping @Sendable (ProxyURLResponse) -> Void) + func sendRequest(_ proxyRequest: ProxyURLRequest) async -> ProxyURLResponse + func cancelRequest(identifier: UUID) +} diff --git a/ios/PacketTunnelCoreTests/ActorTests.swift b/ios/PacketTunnelCoreTests/ActorTests.swift deleted file mode 100644 index c82119759b5a..000000000000 --- a/ios/PacketTunnelCoreTests/ActorTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ActorTests.swift -// PacketTunnelCoreTests -// -// Created by pronebird on 05/09/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Combine -import MullvadSettings -import MullvadTypes -import Network -@testable import PacketTunnelCore -@testable import RelaySelector -import struct WireGuardKitTypes.IPAddressRange -import class WireGuardKitTypes.PrivateKey -import XCTest - -final class ActorTests: XCTestCase { - private var actor: PacketTunnelActor? - private var stateSink: Combine.Cancellable? - - override func tearDown() async throws { - stateSink?.cancel() - actor?.stop() - await actor?.waitUntilDisconnected() - } - - /** - Test a happy path start sequence. - - As actor should transition through the following states: .initial → .connecting → .connected - */ - func testStart() async throws { - let actor = PacketTunnelActor.mock() - let initialStateExpectation = expectation(description: "Expect initial state") - let connectingExpectation = expectation(description: "Expect connecting state") - let connectedStateExpectation = expectation(description: "Expect connected state") - - let allExpectations = [initialStateExpectation, connectingExpectation, connectedStateExpectation] - - stateSink = await actor.$state - .receive(on: DispatchQueue.main) - .sink { newState in - switch newState { - case .initial: - initialStateExpectation.fulfill() - case .connecting: - connectingExpectation.fulfill() - case .connected: - connectedStateExpectation.fulfill() - default: - break - } - } - - self.actor = actor - - actor.start(options: StartOptions(launchSource: .app)) - - await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) - } - - /** - Test stopping connected tunnel. - - As actor should transition through the following states: .connected → .disconnecting → .disconnected - */ - func testStopConnectedTunnel() async throws { - let actor = PacketTunnelActor.mock() - let connectedStateExpectation = expectation(description: "Expect connected state") - let disconnectingStateExpectation = expectation(description: "Expect disconnecting state") - let disconnectedStateExpectation = expectation(description: "Expect disconnected state") - - let allExpectations = [connectedStateExpectation, disconnectingStateExpectation, disconnectedStateExpectation] - - stateSink = await actor.$state - .receive(on: DispatchQueue.main) - .sink { newState in - switch newState { - case .connected: - connectedStateExpectation.fulfill() - actor.stop() - - case .disconnecting: - disconnectingStateExpectation.fulfill() - - case .disconnected: - disconnectedStateExpectation.fulfill() - - default: - break - } - } - - self.actor = actor - - actor.start(options: StartOptions(launchSource: .app)) - - await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) - } - - /** - Each subsequent connection attempt should produce a single change to `state` containing the incremented attempt counter and new relay. - - .connecting (attempt: 0) → .connecting (attempt: 1) → .connecting (attempt: 2) → ... - */ - func testConnectionAttemptTransition() async throws { - let tunnelMonitor = TunnelMonitorStub { _, _ in } - let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) - let connectingStateExpectation = expectation(description: "Expect connecting state") - connectingStateExpectation.expectedFulfillmentCount = 5 - - var nextAttemptCount: UInt = 0 - stateSink = await actor.$state - .receive(on: DispatchQueue.main) - .sink { newState in - switch newState { - case .initial: - break - - case let .connecting(connState): - XCTAssertEqual(connState.connectionAttemptCount, nextAttemptCount) - nextAttemptCount += 1 - connectingStateExpectation.fulfill() - - if nextAttemptCount < connectingStateExpectation.expectedFulfillmentCount { - tunnelMonitor.dispatch(.connectionLost, after: .milliseconds(10)) - } - - default: - XCTFail("Received invalid state: \(newState.name).") - } - } - - self.actor = actor - - actor.start(options: StartOptions(launchSource: .app)) - - await fulfillment(of: [connectingStateExpectation], timeout: 1) - } - - /** - Each subsequent re-connection attempt should produce a single change to `state` containing the incremented attempt counter and new relay. - - .reconnecting (attempt: 0) → .reconnecting (attempt: 1) → .reconnecting (attempt: 2) → ... - */ - func testReconnectionAttemptTransition() async throws { - let tunnelMonitor = TunnelMonitorStub { _, _ in } - let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) - let connectingStateExpectation = expectation(description: "Expect connecting state") - let connectedStateExpectation = expectation(description: "Expect connected state") - let reconnectingStateExpectation = expectation(description: "Expect reconnecting state") - reconnectingStateExpectation.expectedFulfillmentCount = 5 - - var nextAttemptCount: UInt = 0 - stateSink = await actor.$state - .receive(on: DispatchQueue.main) - .sink { newState in - switch newState { - case .initial: - break - - case .connecting: - connectingStateExpectation.fulfill() - tunnelMonitor.dispatch(.connectionEstablished, after: .milliseconds(10)) - - case .connected: - connectedStateExpectation.fulfill() - tunnelMonitor.dispatch(.connectionLost, after: .milliseconds(10)) - - case let .reconnecting(connState): - XCTAssertEqual(connState.connectionAttemptCount, nextAttemptCount) - nextAttemptCount += 1 - reconnectingStateExpectation.fulfill() - - if nextAttemptCount < reconnectingStateExpectation.expectedFulfillmentCount { - tunnelMonitor.dispatch(.connectionLost, after: .milliseconds(10)) - } - - default: - XCTFail("Received invalid state: \(newState.name).") - } - } - - self.actor = actor - - actor.start(options: StartOptions(launchSource: .app)) - - await fulfillment( - of: [connectingStateExpectation, connectedStateExpectation, reconnectingStateExpectation], - timeout: 1, - enforceOrder: true - ) - } -} diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift new file mode 100644 index 000000000000..bea5a83e8b02 --- /dev/null +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -0,0 +1,114 @@ +// +// AppMessageHandlerTests.swift +// PacketTunnelCoreTests +// +// Created by Jon Petersson on 2023-09-28. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +@testable import MullvadREST +import MullvadTypes +import PacketTunnelCore +import RelaySelector +import XCTest + +final class AppMessageHandlerTests: XCTestCase { + func testHandleAppMessageForSendURLRequest() async throws { + let sendRequestExpectation = expectation(description: "Expect sending request") + + let actor = PacketTunnelActorStub() + let urlRequestProxy = URLRequestProxyStub(sendRequestExpectation: sendRequestExpectation) + let appMessageHandler = createAppMessageHandler(urlRequestProxy: urlRequestProxy) + + let url = URL(string: "localhost")! + let urlRequest = ProxyURLRequest( + id: UUID(), + urlRequest: URLRequest(url: url) + )! + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.sendURLRequest(urlRequest).encode() + ) + + await fulfillment(of: [sendRequestExpectation], timeout: 1) + } + + func testHandleAppMessageForCancelURLRequest() async throws { + let cancelRequestExpectation = expectation(description: "Expect cancelling request") + + let actor = PacketTunnelActorStub() + let urlRequestProxy = URLRequestProxyStub(cancelRequestExpectation: cancelRequestExpectation) + let appMessageHandler = createAppMessageHandler(urlRequestProxy: urlRequestProxy) + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.cancelURLRequest(UUID()).encode() + ) + + await fulfillment(of: [cancelRequestExpectation], timeout: 1) + } + + func testHandleAppMessageForTunnelStatus() async throws { + let stateExpectation = expectation(description: "Expect getting state") + + let actor = PacketTunnelActorStub(stateExpectation: stateExpectation) + let appMessageHandler = createAppMessageHandler(actor: actor) + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.getTunnelStatus.encode() + ) + + await fulfillment(of: [stateExpectation], timeout: 1) + } + + func testHandleAppMessageForKeyRotation() async throws { + let keyRotationExpectation = expectation(description: "Expect key rotation") + + let actor = PacketTunnelActorStub(keyRotationExpectation: keyRotationExpectation) + let appMessageHandler = createAppMessageHandler(actor: actor) + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.privateKeyRotation.encode() + ) + + await fulfillment(of: [keyRotationExpectation], timeout: 1) + } + + func testHandleAppMessageForReconnectTunnel() async throws { + let reconnectExpectation = expectation(description: "Expect reconnecting state") + + let actor = PacketTunnelActorStub(reconnectExpectation: reconnectExpectation) + let appMessageHandler = createAppMessageHandler(actor: actor) + + let relayConstraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard"))) + let selectorResult = try XCTUnwrap(try? RelaySelector.evaluate( + relays: ServerRelaysResponseStubs.sampleRelays, + constraints: relayConstraints, + numberOfFailedAttempts: 0 + )) + + let selectedRelay = SelectedRelay( + endpoint: selectorResult.endpoint, + hostname: selectorResult.relay.hostname, + location: selectorResult.location + ) + + _ = try? await appMessageHandler.handleAppMessage( + TunnelProviderMessage.reconnectTunnel(.preSelected(selectedRelay)).encode() + ) + + await fulfillment(of: [reconnectExpectation], timeout: 1) + } +} + +extension AppMessageHandlerTests { + func createAppMessageHandler( + actor: PacketTunnelActorProtocol = PacketTunnelActorStub(), + urlRequestProxy: URLRequestProxyProtocol = URLRequestProxyStub() + ) -> AppMessageHandler { + return AppMessageHandler( + packetTunnelActor: actor, + urlRequestProxy: urlRequestProxy + ) + } +} diff --git a/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift index 8031ea888c18..8bde84f7816a 100644 --- a/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift +++ b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift @@ -22,18 +22,22 @@ class DefaultPathObserverFake: DefaultPathObserverProtocol { private var innerPath: NetworkPath = NetworkPathStub() private var stateLock = NSLock() - private var defaultPathHandler: ((NetworkPath) -> Void)? + public var onStart: (() -> Void)? + public var onStop: (() -> Void)? + func start(_ body: @escaping (NetworkPath) -> Void) { stateLock.withLock { defaultPathHandler = body + onStart?() } } func stop() { stateLock.withLock { defaultPathHandler = nil + onStop?() } } diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift index 418f691f46e3..3e01bec31c4d 100644 --- a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift +++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift @@ -13,7 +13,7 @@ extension PacketTunnelActorTimings { static var timingsForTests: PacketTunnelActorTimings { return PacketTunnelActorTimings( bootRecoveryPeriodicity: .milliseconds(10), - wgKeyPropagationDelay: .milliseconds(10) + wgKeyPropagationDelay: .zero ) } } diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift new file mode 100644 index 000000000000..80c22971df6a --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift @@ -0,0 +1,33 @@ +// +// PacketTunnelActorStub.swift +// PacketTunnelCoreTests +// +// Created by Jon Petersson on 2023-10-11. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import PacketTunnelCore +import XCTest + +struct PacketTunnelActorStub: PacketTunnelActorProtocol { + let innerState: State = .disconnected + var stateExpectation: XCTestExpectation? + var reconnectExpectation: XCTestExpectation? + var keyRotationExpectation: XCTestExpectation? + + var state: State { + get async { + stateExpectation?.fulfill() + return innerState + } + } + + func reconnect(to nextRelay: NextRelay) { + reconnectExpectation?.fulfill() + } + + func notifyKeyRotation(date: Date?) { + keyRotationExpectation?.fulfill() + } +} diff --git a/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift b/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift new file mode 100644 index 000000000000..a8f21538dbe9 --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift @@ -0,0 +1,32 @@ +// +// URLRequestProxyStub.swift +// PacketTunnelCoreTests +// +// Created by Jon Petersson on 2023-10-11. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import PacketTunnelCore +import XCTest + +struct URLRequestProxyStub: URLRequestProxyProtocol { + var sendRequestExpectation: XCTestExpectation? + var cancelRequestExpectation: XCTestExpectation? + + func sendRequest( + _ proxyRequest: PacketTunnelCore.ProxyURLRequest, + completionHandler: @escaping @Sendable (PacketTunnelCore.ProxyURLResponse) -> Void + ) { + sendRequestExpectation?.fulfill() + } + + func sendRequest(_ proxyRequest: PacketTunnelCore.ProxyURLRequest) async -> PacketTunnelCore.ProxyURLResponse { + sendRequestExpectation?.fulfill() + return ProxyURLResponse(data: nil, response: nil, error: nil) + } + + func cancelRequest(identifier: UUID) { + cancelRequestExpectation?.fulfill() + } +} diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift new file mode 100644 index 000000000000..3bdf0b062f7f --- /dev/null +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -0,0 +1,436 @@ +// +// PacketTunnelActorTests.swift +// PacketTunnelCoreTests +// +// Created by pronebird on 05/09/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +import MullvadSettings +import MullvadTypes +import Network +@testable import PacketTunnelCore +@testable import RelaySelector +import struct WireGuardKitTypes.IPAddressRange +import class WireGuardKitTypes.PrivateKey +import XCTest + +final class PacketTunnelActorTests: XCTestCase { + private var stateSink: Combine.Cancellable? + private let launchOptions = StartOptions(launchSource: .app) + + override func tearDown() async throws { + stateSink?.cancel() + } + + /** + Test a happy path start sequence. + + As actor should transition through the following states: .initial → .connecting → .connected + */ + func testStartGoesToConnectedInSequence() async throws { + let actor = PacketTunnelActor.mock() + + // As actor starts it should transition through the following states based on simulation: + // .initial → .connecting → .connected + let initialStateExpectation = expectation(description: "Expect initial state") + let connectingExpectation = expectation(description: "Expect connecting state") + let connectedStateExpectation = expectation(description: "Expect connected state") + + let allExpectations = [initialStateExpectation, connectingExpectation, connectedStateExpectation] + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .initial: + initialStateExpectation.fulfill() + case .connecting: + connectingExpectation.fulfill() + case .connected: + connectedStateExpectation.fulfill() + default: + break + } + } + + actor.start(options: launchOptions) + + await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) + } + + func testStartIgnoresSubsequentStarts() async throws { + let actor = PacketTunnelActor.mock() + + // As actor starts it should transition through the following states based on simulation: + // .initial → .connecting → .connected + let initialStateExpectation = expectation(description: "Expect initial state") + let connectingExpectation = expectation(description: "Expect connecting state") + let connectedStateExpectation = expectation(description: "Expect connected state") + + let allExpectations = [initialStateExpectation, connectingExpectation, connectedStateExpectation] + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .initial: + initialStateExpectation.fulfill() + case .connecting: + connectingExpectation.fulfill() + case .connected: + connectedStateExpectation.fulfill() + default: + break + } + } + + actor.start(options: launchOptions) + actor.start(options: launchOptions) + + await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) + } + + /** + Each subsequent connection attempt should produce a single change to `state` containing the incremented attempt counter and new relay. + .connecting (attempt: 0) → .connecting (attempt: 1) → .connecting (attempt: 2) → ... + */ + func testConnectionAttemptTransition() async throws { + let tunnelMonitor = TunnelMonitorStub { _, _ in } + let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) + let connectingStateExpectation = expectation(description: "Expect connecting state") + connectingStateExpectation.expectedFulfillmentCount = 5 + var nextAttemptCount: UInt = 0 + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .initial: + break + case let .connecting(connState): + XCTAssertEqual(connState.connectionAttemptCount, nextAttemptCount) + nextAttemptCount += 1 + connectingStateExpectation.fulfill() + if nextAttemptCount < connectingStateExpectation.expectedFulfillmentCount { + tunnelMonitor.dispatch(.connectionLost, after: .milliseconds(10)) + } + default: + XCTFail("Received invalid state: \(newState.name).") + } + } + + actor.start(options: StartOptions(launchSource: .app)) + await fulfillment(of: [connectingStateExpectation], timeout: 1) + } + + /** + Each subsequent re-connection attempt should produce a single change to `state` containing the incremented attempt counter and new relay. + .reconnecting (attempt: 0) → .reconnecting (attempt: 1) → .reconnecting (attempt: 2) → ... + */ + func testReconnectionAttemptTransition() async throws { + let tunnelMonitor = TunnelMonitorStub { _, _ in } + let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) + let connectingStateExpectation = expectation(description: "Expect connecting state") + let connectedStateExpectation = expectation(description: "Expect connected state") + let reconnectingStateExpectation = expectation(description: "Expect reconnecting state") + reconnectingStateExpectation.expectedFulfillmentCount = 5 + var nextAttemptCount: UInt = 0 + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .initial: + break + case .connecting: + connectingStateExpectation.fulfill() + tunnelMonitor.dispatch(.connectionEstablished, after: .milliseconds(10)) + case .connected: + connectedStateExpectation.fulfill() + tunnelMonitor.dispatch(.connectionLost, after: .milliseconds(10)) + case let .reconnecting(connState): + XCTAssertEqual(connState.connectionAttemptCount, nextAttemptCount) + nextAttemptCount += 1 + reconnectingStateExpectation.fulfill() + if nextAttemptCount < reconnectingStateExpectation.expectedFulfillmentCount { + tunnelMonitor.dispatch(.connectionLost, after: .milliseconds(10)) + } + default: + XCTFail("Received invalid state: \(newState.name).") + } + } + + actor.start(options: StartOptions(launchSource: .app)) + await fulfillment( + of: [connectingStateExpectation, connectedStateExpectation, reconnectingStateExpectation], + timeout: 1, + enforceOrder: true + ) + } + + /** + Test start sequence when reading settings yields an error indicating that device is locked. + This is common when network extenesion starts on boot with iOS. + + 1. The first attempt to read settings yields an error indicating that device is locked. + 2. An actor should set up a task to reconnect the tunnel periodically. + 3. The issue goes away on the second attempt to read settings. + 4. An actor should transition through `.connecting` towards`.connected` state. + */ + func testLockedDeviceErrorOnBoot() async throws { + let initialStateExpectation = expectation(description: "Expect initial state") + let errorStateExpectation = expectation(description: "Expect error state") + let connectingStateExpectation = expectation(description: "Expect connecting state") + let connectedStateExpectation = expectation(description: "Expect connected state") + let allExpectations = [ + initialStateExpectation, + errorStateExpectation, + connectingStateExpectation, + connectedStateExpectation, + ] + + let blockedStateMapper = BlockedStateErrorMapperStub { error in + if let error = error as? POSIXError, error.code == .EPERM { + return .deviceLocked + } else { + return .unknown + } + } + + var isFirstReadAttempt = true + let settingsReader = SettingsReaderStub { + if isFirstReadAttempt { + isFirstReadAttempt = false + throw POSIXError(.EPERM) + } else { + return Settings( + privateKey: PrivateKey(), + interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], + relayConstraints: RelayConstraints(), + dnsServers: .gateway + ) + } + } + + let actor = PacketTunnelActor.mock(blockedStateErrorMapper: blockedStateMapper, settingsReader: settingsReader) + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .initial: + initialStateExpectation.fulfill() + case .error: + errorStateExpectation.fulfill() + case .connecting: + connectingStateExpectation.fulfill() + case .connected: + connectedStateExpectation.fulfill() + default: + break + } + } + + actor.start(options: launchOptions) + + await fulfillment(of: allExpectations, timeout: 1, enforceOrder: true) + } + + func testStopGoesToDisconnected() async throws { + let actor = PacketTunnelActor.mock() + let disconnectedStateExpectation = expectation(description: "Expect disconnected state") + let connectedStateExpectation = expectation(description: "Expect connected state") + + let expression: (State) -> Bool = { if case .connected = $0 { true } else { false } } + + await expect(expression, on: actor) { + connectedStateExpectation.fulfill() + } + + // Wait for the connected state to happen so it doesn't get coalesced immediately after the call to `actor.stop` + actor.start(options: launchOptions) + await fulfillment(of: [connectedStateExpectation], timeout: 1) + + await expect(.disconnected, on: actor) { + disconnectedStateExpectation.fulfill() + } + actor.stop() + await fulfillment(of: [disconnectedStateExpectation], timeout: 1) + } + + func testStopIsNoopBeforeStart() async throws { + let actor = PacketTunnelActor.mock() + + let disconnectedExpectation = expectation(description: "Disconnected state") + disconnectedExpectation.isInverted = true + + await expect(.disconnected, on: actor) { + disconnectedExpectation.fulfill() + } + + actor.stop() + actor.stop() + actor.stop() + + await fulfillment(of: [disconnectedExpectation], timeout: Duration.milliseconds(100).timeInterval) + } + + func testStopCancelsDefaultPathObserver() async throws { + let pathObserver = DefaultPathObserverFake() + let actor = PacketTunnelActor.mock(defaultPathObserver: pathObserver) + + let connectedStateExpectation = expectation(description: "Connected state") + let didStopObserverExpectation = expectation(description: "Did stop path observer") + didStopObserverExpectation.expectedFulfillmentCount = 2 + pathObserver.onStop = { didStopObserverExpectation.fulfill() } + + let expression: (State) -> Bool = { if case .connected = $0 { true } else { false } } + + await expect(expression, on: actor) { + connectedStateExpectation.fulfill() + } + + actor.start(options: launchOptions) + await fulfillment(of: [connectedStateExpectation], timeout: 1) + + let disconnectedStateExpectation = expectation(description: "Disconnected state") + + await expect(.disconnected, on: actor) { + disconnectedStateExpectation.fulfill() + } + actor.stop() + await fulfillment(of: [disconnectedStateExpectation, didStopObserverExpectation], timeout: 1) + } + + func testSetErrorStateGetsCancelledWhenStopping() async throws { + let actor = PacketTunnelActor.mock() + let connectingStateExpectation = expectation(description: "Connecting state") + let disconnectedStateExpectation = expectation(description: "Disconnected state") + let errorStateExpectation = expectation(description: "Should not enter error state") + errorStateExpectation.isInverted = true + + stateSink = await actor.$state + .receive(on: DispatchQueue.main) + .sink { newState in + switch newState { + case .connecting: + actor.setErrorState(reason: .readSettings) + connectingStateExpectation.fulfill() + case .error: + errorStateExpectation.fulfill() + case .disconnected: + disconnectedStateExpectation.fulfill() + default: + break + } + } + + actor.start(options: launchOptions) + actor.stop() + + await fulfillment(of: [connectingStateExpectation, disconnectedStateExpectation], timeout: 1) + await fulfillment(of: [errorStateExpectation], timeout: Duration.milliseconds(100).timeInterval) + } + + func testReconnectIsNoopBeforeConnecting() async throws { + let actor = PacketTunnelActor.mock() + let reconnectingStateExpectation = expectation(description: "Expect initial state") + reconnectingStateExpectation.isInverted = true + + let expression: (State) -> Bool = { if case .reconnecting = $0 { true } else { false } } + + await expect(expression, on: actor) { + reconnectingStateExpectation.fulfill() + } + + actor.reconnect(to: .random) + + await fulfillment( + of: [reconnectingStateExpectation], + timeout: Duration.milliseconds(100).timeInterval + ) + } + + func testCannotReconnectAfterStopping() async throws { + let actor = PacketTunnelActor.mock() + + let disconnectedStateExpectation = expectation(description: "Expect disconnected state") + + await expect(.disconnected, on: actor) { disconnectedStateExpectation.fulfill() } + + actor.start(options: launchOptions) + actor.stop() + + await fulfillment(of: [disconnectedStateExpectation], timeout: 1) + + let reconnectingStateExpectation = expectation(description: "Expect initial state") + reconnectingStateExpectation.isInverted = true + let expression: (State) -> Bool = { if case .reconnecting = $0 { true } else { false } } + + await expect(expression, on: actor) { reconnectingStateExpectation.fulfill() } + + actor.reconnect(to: .random) + await fulfillment( + of: [reconnectingStateExpectation], + timeout: Duration.milliseconds(100).timeInterval + ) + } + + func testReconnectionStopsTunnelMonitor() async throws { + let stopMonitorExpectation = expectation(description: "Tunnel monitor stop") + + let tunnelMonitor = TunnelMonitorStub { command, dispatcher in + switch command { + case .start: + dispatcher.send(.connectionEstablished, after: .milliseconds(10)) + case .stop: + stopMonitorExpectation.fulfill() + } + } + let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) + let connectedExpectation = expectation(description: "Expect connected state") + + let expression: (State) -> Bool = { if case .connected = $0 { return true } else { return false } } + await expect(expression, on: actor) { + connectedExpectation.fulfill() + } + actor.start(options: launchOptions) + await fulfillment(of: [connectedExpectation], timeout: 1) + + // Cancel the state sink to avoid overfulfilling the connected expectation + stateSink?.cancel() + + actor.reconnect(to: .random) + await fulfillment(of: [stopMonitorExpectation], timeout: 1) + } +} + +extension PacketTunnelActorTests { + func expect(_ state: State, on actor: PacketTunnelActor, _ action: @escaping () -> Void) async { + stateSink = await actor.$state.receive(on: DispatchQueue.main).sink { newState in + if state == newState { + action() + } + } + } + + func expect( + _ expression: @escaping (State) -> Bool, + on actor: PacketTunnelActor, + _ action: @escaping () -> Void + ) async { + stateSink = await actor.$state.receive(on: DispatchQueue.main).sink { newState in + if expression(newState) { + action() + } + } + } +} + +extension State: Equatable { + public static func == (lhs: State, rhs: State) -> Bool { + lhs.name == rhs.name + } +} + +// swiftlint:disable:this file_length diff --git a/ios/RelayCache/RelayCache.swift b/ios/RelayCache/RelayCache.swift index abce82378601..ba7ed31cf1cd 100644 --- a/ios/RelayCache/RelayCache.swift +++ b/ios/RelayCache/RelayCache.swift @@ -10,7 +10,12 @@ import Foundation import MullvadREST import MullvadTypes -public final class RelayCache { +public protocol RelayCacheProtocol { + func read() throws -> CachedRelays + func write(record: CachedRelays) throws +} + +public final class RelayCache: RelayCacheProtocol { private let fileCache: any FileCacheProtocol /// Designated initializer