diff options
19 files changed, 654 insertions, 98 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 246bbb20dc..3f30895939 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 063687B028EB083800BE7161 /* ProxyURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */; }; + 063687B228EB083F00BE7161 /* ProxyURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */; }; + 063687B528EB22E000BE7161 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B428EB22E000BE7161 /* RESTTransport.swift */; }; + 063687B828EB231900BE7161 /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B728EB231900BE7161 /* URLSessionTransport.swift */; }; + 063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */; }; + 063687BC28EEC00800BE7161 /* RESTTransportRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687BB28EEC00800BE7161 /* RESTTransportRegistry.swift */; }; + 0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; }; 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; @@ -185,6 +192,9 @@ 589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E1E292848DF67004838B3 /* OperationObserverTests.swift */; }; 589A455E28E094BF00565204 /* OperationInputInjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */; }; 589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; }; + 589E63D728F7161F005FAB05 /* RESTURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F7C280D6FE000013055 /* RESTURLSession.swift */; }; + 589E63D828F71626005FAB05 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; + 589E63D928F71649005FAB05 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* le_root_cert.cer */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; }; 58A8055E2716EA6700681642 /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; }; @@ -386,6 +396,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyURLRequest.swift; sourceTree = "<group>"; }; + 063687B428EB22E000BE7161 /* RESTTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; }; + 063687B728EB231900BE7161 /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; }; + 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelTransport.swift; sourceTree = "<group>"; }; + 063687BB28EEC00800BE7161 /* RESTTransportRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportRegistry.swift; sourceTree = "<group>"; }; + 0697D6E628F01513007A9E99 /* TransportMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportMonitor.swift; sourceTree = "<group>"; }; 58059DDB28465E8F002B1049 /* TransformOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; }; 58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; }; 58059DDF2846823E002B1049 /* ResultOperation+Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultOperation+Output.swift"; sourceTree = "<group>"; }; @@ -762,6 +778,7 @@ 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */, 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */, + 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */, 58B93A1226C3F13600A55733 /* TunnelState.swift */, 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */, 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, @@ -825,9 +842,12 @@ 588BCF272816D664009ADCEC /* RESTResponseHandler.swift */, 58095C582762155700890776 /* RESTRetryStrategy.swift */, 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */, + 063687B428EB22E000BE7161 /* RESTTransport.swift */, + 063687BB28EEC00800BE7161 /* RESTTransportRegistry.swift */, 58554F7C280D6FE000013055 /* RESTURLSession.swift */, 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */, 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */, + 063687B728EB231900BE7161 /* URLSessionTransport.swift */, ); path = REST; sourceTree = "<group>"; @@ -860,6 +880,15 @@ path = OperationsTests; sourceTree = "<group>"; }; + 589E63DA28F7E9E7005FAB05 /* TransportMonitor */ = { + isa = PBXGroup; + children = ( + 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */, + 0697D6E628F01513007A9E99 /* TransportMonitor.swift */, + ); + path = TransportMonitor; + sourceTree = "<group>"; + }; 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = { isa = PBXGroup; children = ( @@ -992,6 +1021,7 @@ 585DA87526B0249A00B8C587 /* RelayCache */, 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, 58781CD422AFBA39009B9D8E /* RelaySelector.swift */, + 589E63DA28F7E9E7005FAB05 /* TransportMonitor */, 585DA87F26B0268500B8C587 /* REST */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */, @@ -1383,6 +1413,7 @@ buildActionMask = 2147483647; files = ( 58F3C0A724A50C02003E76BE /* relays.json in Resources */, + 589E63D928F71649005FAB05 /* le_root_cert.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1472,6 +1503,7 @@ 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */, + 063687BC28EEC00800BE7161 /* RESTTransportRegistry.swift in Sources */, 5872631B283F6EAB00E14ADF /* Intents.intentdefinition in Sources */, 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, @@ -1489,6 +1521,7 @@ E1187ABF289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */, + 0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */, 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */, 5820675026E6514100655B05 /* HTTP.swift in Sources */, 584D26C2270C8542004EA533 /* SettingsStaticTextFooterView.swift in Sources */, @@ -1573,6 +1606,7 @@ 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */, 5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, + 063687B028EB083800BE7161 /* ProxyURLRequest.swift in Sources */, 753D6C0C28B4BF3E0052D9E1 /* ShortcutsManager.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, 5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */, @@ -1592,6 +1626,7 @@ 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */, 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, + 063687B528EB22E000BE7161 /* RESTTransport.swift in Sources */, 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */, 75FD0C2328B109860021E33E /* ShortcutsDataSourceDelegate.swift in Sources */, 58E511EB28DDE18400B0BCDE /* Error+Chain.swift in Sources */, @@ -1604,8 +1639,10 @@ 58B5A895280AACC4009FDE99 /* RESTRequestFactory.swift in Sources */, 584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */, 5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */, + 063687B828EB231900BE7161 /* URLSessionTransport.swift in Sources */, 58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */, 585DA88726B0277200B8C587 /* RESTError.swift in Sources */, + 063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */, 58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, 585DA87726B024A600B8C587 /* CachedRelays.swift in Sources */, @@ -1662,12 +1699,15 @@ 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, + 589E63D728F7161F005FAB05 /* RESTURLSession.swift in Sources */, 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */, 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, + 063687B228EB083F00BE7161 /* ProxyURLRequest.swift in Sources */, 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */, + 589E63D828F71626005FAB05 /* SSLPinningURLSessionDelegate.swift in Sources */, 5820675626E6528A00655B05 /* RESTError.swift in Sources */, 58900D0328BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift in Sources */, 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, @@ -1875,7 +1915,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1938,7 +1977,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Apple Distribution"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 92c99794fa..422814b64a 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -28,6 +28,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return operationQueue }() + private let transportMonitor = TransportMonitor() + // MARK: - Application lifecycle func application( diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index f0e8efee4c..618e293192 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -51,6 +51,13 @@ extension REST.Error: DisplayChainedError { value: "Server response decoding error", comment: "" ) + case let .transport(error): + return NSLocalizedString( + "TRANSPORT_ERROR", + tableName: "REST", + value: "Transport error: \(error.localizedDescription)", + comment: "" + ) } } } diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift index 63a6167d39..d813680a9b 100644 --- a/ios/MullvadVPN/REST/RESTError.swift +++ b/ios/MullvadVPN/REST/RESTError.swift @@ -11,24 +11,29 @@ import Foundation extension REST { /// An error type returned by REST API classes. enum Error: LocalizedError, WrappingError { - /// A failure to create URL request. + /// Failure to create URL request. case createURLRequest(Swift.Error) - /// A failure during networking. + /// Network failure. case network(URLError) - /// A failure to handle response. + /// Failure to handle response. case unhandledResponse(_ statusCode: Int, _ serverResponse: ServerErrorResponse?) - /// A failure to decode server response. + /// Failure to decode server response. case decodeResponse(Swift.Error) + /// Failure to transit URL request via selected transport implementation. + case transport(Swift.Error) + var errorDescription: String? { switch self { case let .createURLRequest(error): return "Failure to create URL request: \(error.localizedDescription)." + case let .network(error): return "Network error: \(error.localizedDescription)." + case let .unhandledResponse(statusCode, serverResponse): var str = "Failure to handle server response: HTTP/\(statusCode)." @@ -41,8 +46,12 @@ extension REST { } return str + case let .decodeResponse(error): return "Failure to decode URL response data: \(error.localizedDescription)." + + case let .transport(error): + return "Transport error: \(error.localizedDescription)." } } @@ -54,6 +63,8 @@ extension REST { return error case let .decodeResponse(error): return error + case let .transport(error): + return error case .unhandledResponse: return nil } @@ -100,4 +111,10 @@ extension REST { self.rawValue = rawValue } } + + struct NoTransportError: LocalizedError { + var errorDescription: String? { + return "Transport is not configured." + } + } } diff --git a/ios/MullvadVPN/REST/RESTNetworkOperation.swift b/ios/MullvadVPN/REST/RESTNetworkOperation.swift index 26ab4cf220..00544390b4 100644 --- a/ios/MullvadVPN/REST/RESTNetworkOperation.swift +++ b/ios/MullvadVPN/REST/RESTNetworkOperation.swift @@ -16,10 +16,10 @@ extension REST { private let responseHandler: AnyResponseHandler<Success> private let logger: Logger - private let urlSession: URLSession + private let transportRegistry: RESTTransportRegistry private let addressCacheStore: AddressCache.Store - private var networkTask: URLSessionTask? + private var networkTask: Cancellable? private var authorizationTask: Cancellable? private var requiresAuthorization = false @@ -38,8 +38,8 @@ extension REST { responseHandler: AnyResponseHandler<Success>, completionHandler: @escaping CompletionHandler ) { - urlSession = configuration.session addressCacheStore = configuration.addressCacheStore + transportRegistry = configuration.transportRegistry self.retryStrategy = retryStrategy self.requestHandler = requestHandler self.responseHandler = responseHandler @@ -136,30 +136,47 @@ extension REST { private func didReceiveURLRequest(_ restRequest: REST.Request, endpoint: AnyIPEndpoint) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) - logger - .debug( - "Send request to \(restRequest.pathTemplate.templateString) via \(endpoint)." - ) + guard let transport = transportRegistry.getTransport() else { + logger.error("Failed to obtain transport.") + finish(completion: .failure(.transport(NoTransportError()))) + return + } - networkTask = urlSession - .dataTask(with: restRequest.urlRequest) { [weak self] data, response, error in - guard let self = self else { return } + logger.debug( + """ + Send request to \(restRequest.pathTemplate.templateString) via \(endpoint) \ + using \(transport.name). + """ + ) - self.dispatchQueue.async { - if let error = error { - let urlError = error as! URLError + do { + networkTask = try transport + .sendRequest(restRequest.urlRequest) { [weak self] data, response, error in + guard let self = self else { return } - self.didReceiveURLError(urlError, endpoint: endpoint) - } else { - let httpResponse = response as! HTTPURLResponse - let data = data ?? Data() + self.dispatchQueue.async { + if let error = error { + self.didReceiveError( + error, + transport: transport, + endpoint: endpoint + ) + } else { + let httpResponse = response as! HTTPURLResponse + let data = data ?? Data() - self.didReceiveURLResponse(httpResponse, data: data, endpoint: endpoint) + self.didReceiveURLResponse( + httpResponse, + transport: transport, + data: data, + endpoint: endpoint + ) + } } } - } - - networkTask?.resume() + } catch { + didReceiveError(error, transport: transport, endpoint: endpoint) + } } private func didFailToCreateURLRequest(_ error: REST.Error) { @@ -173,64 +190,38 @@ extension REST { finish(completion: .failure(error)) } - private func didReceiveURLError(_ urlError: URLError, endpoint: AnyIPEndpoint) { + private func didReceiveError( + _ error: Swift.Error, + transport: RESTTransport, + endpoint: AnyIPEndpoint + ) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) - switch urlError.code { - case .cancelled: - finish(completion: .cancelled) - return + if let urlError = error as? URLError { + switch urlError.code { + case .cancelled: + finish(completion: .cancelled) + return - case .notConnectedToInternet, .internationalRoamingOff, .callIsActive: - break + case .notConnectedToInternet, .internationalRoamingOff, .callIsActive: + break - default: - _ = addressCacheStore.selectNextEndpoint(endpoint) + default: + _ = addressCacheStore.selectNextEndpoint(endpoint) + } } logger.error( - error: urlError, - message: "Failed to perform request to \(endpoint)." + error: error, + message: "Failed to perform request to \(endpoint) using \(transport.name)." ) - // Check if retry count is not exceeded. - guard retryCount < retryStrategy.maxRetryCount else { - if retryStrategy.maxRetryCount > 0 { - logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))") - } - - finish(completion: OperationCompletion(result: .failure(.network(urlError)))) - return - } - - // Increment retry count. - retryCount += 1 - - // Retry immediatly if retry delay is set to never. - guard retryStrategy.retryDelay != .never else { - startRequest() - return - } - - // Create timer to delay retry. - let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) - - timer.setEventHandler { [weak self] in - self?.startRequest() - } - - timer.setCancelHandler { [weak self] in - self?.finish(completion: .cancelled) - } - - timer.schedule(wallDeadline: .now() + retryStrategy.retryDelay) - timer.activate() - - retryTimer = timer + retryRequest(with: error) } private func didReceiveURLResponse( _ response: HTTPURLResponse, + transport: RESTTransport, data: Data, endpoint: AnyIPEndpoint ) { @@ -271,5 +262,45 @@ extension REST { } } } + + private func retryRequest(with error: Swift.Error) { + // Check if retry count is not exceeded. + guard retryCount < retryStrategy.maxRetryCount else { + if retryStrategy.maxRetryCount > 0 { + logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))") + } + + let restError: REST.Error = (error as? URLError).map { .network($0) } + ?? .transport(error) + + finish(completion: .failure(restError)) + return + } + + // Increment retry count. + retryCount += 1 + + // Retry immediatly if retry delay is set to never. + guard retryStrategy.retryDelay != .never else { + startRequest() + return + } + + // Create timer to delay retry. + let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) + + timer.setEventHandler { [weak self] in + self?.startRequest() + } + + timer.setCancelHandler { [weak self] in + self?.finish(completion: .cancelled) + } + + timer.schedule(wallDeadline: .now() + retryStrategy.retryDelay) + timer.activate() + + retryTimer = timer + } } } diff --git a/ios/MullvadVPN/REST/RESTProxy.swift b/ios/MullvadVPN/REST/RESTProxy.swift index 6dc2c08a33..5036cf9a50 100644 --- a/ios/MullvadVPN/REST/RESTProxy.swift +++ b/ios/MullvadVPN/REST/RESTProxy.swift @@ -66,11 +66,11 @@ extension REST { } class ProxyConfiguration { - let session: URLSession + let transportRegistry: RESTTransportRegistry let addressCacheStore: AddressCache.Store - init(session: URLSession, addressCacheStore: AddressCache.Store) { - self.session = session + init(transportRegistry: RESTTransportRegistry, addressCacheStore: AddressCache.Store) { + self.transportRegistry = transportRegistry self.addressCacheStore = addressCacheStore } } @@ -82,7 +82,7 @@ extension REST { self.accessTokenManager = accessTokenManager super.init( - session: proxyConfiguration.session, + transportRegistry: proxyConfiguration.transportRegistry, addressCacheStore: proxyConfiguration.addressCacheStore ) } diff --git a/ios/MullvadVPN/REST/RESTProxyFactory.swift b/ios/MullvadVPN/REST/RESTProxyFactory.swift index 34eeee6776..5056a8b83e 100644 --- a/ios/MullvadVPN/REST/RESTProxyFactory.swift +++ b/ios/MullvadVPN/REST/RESTProxyFactory.swift @@ -14,7 +14,7 @@ extension REST { static let shared: ProxyFactory = { let basicConfiguration = ProxyConfiguration( - session: REST.sharedURLSession, + transportRegistry: RESTTransportRegistry.shared, addressCacheStore: AddressCache.Store.shared ) diff --git a/ios/MullvadVPN/REST/RESTTransport.swift b/ios/MullvadVPN/REST/RESTTransport.swift new file mode 100644 index 0000000000..81e6fbb5c2 --- /dev/null +++ b/ios/MullvadVPN/REST/RESTTransport.swift @@ -0,0 +1,18 @@ +// +// RESTTransport.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-10-03. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol RESTTransport: AnyObject { + var name: String { get } + + func sendRequest( + _ request: URLRequest, + completion: @escaping (Data?, URLResponse?, Error?) -> Void + ) throws -> Cancellable +} diff --git a/ios/MullvadVPN/REST/RESTTransportRegistry.swift b/ios/MullvadVPN/REST/RESTTransportRegistry.swift new file mode 100644 index 0000000000..dc04b23f7a --- /dev/null +++ b/ios/MullvadVPN/REST/RESTTransportRegistry.swift @@ -0,0 +1,32 @@ +// +// RESTTransportRegistry.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-10-06. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class RESTTransportRegistry { + static let shared = RESTTransportRegistry() + + private var transport: RESTTransport? + private let nslock = NSLock() + + private init() {} + + func setTransport(_ transport: RESTTransport) { + nslock.lock() + defer { nslock.unlock() } + + self.transport = transport + } + + func getTransport() -> RESTTransport? { + nslock.lock() + defer { nslock.unlock() } + + return transport + } +} diff --git a/ios/MullvadVPN/REST/URLSessionTransport.swift b/ios/MullvadVPN/REST/URLSessionTransport.swift new file mode 100644 index 0000000000..8a48c20c46 --- /dev/null +++ b/ios/MullvadVPN/REST/URLSessionTransport.swift @@ -0,0 +1,32 @@ +// +// URLSessionTransport.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-10-03. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension URLSessionTask: Cancellable {} + +final class URLSessionTransport: RESTTransport { + var name: String { + return "url-session" + } + + let urlSession: URLSession + + init(urlSession: URLSession) { + self.urlSession = urlSession + } + + func sendRequest( + _ request: URLRequest, + completion: @escaping (Data?, URLResponse?, Error?) -> Void + ) throws -> Cancellable { + let dataTask = urlSession.dataTask(with: request, completionHandler: completion) + dataTask.resume() + return dataTask + } +} diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index c30dda0e39..795743fa1e 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -14,6 +14,7 @@ import enum NetworkExtension.NEProviderStopReason class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { private var selectorResult: RelaySelectorResult? + private var proxiedRequests = [UUID: URLSessionDataTask]() private let providerLogger = Logger(label: "SimulatorTunnelProviderHost") private let dispatchQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue") @@ -67,13 +68,13 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { dispatchQueue.async { do { - let response = try self.processMessage(messageData) + let message = try TunnelProviderMessage(messageData: messageData) - completionHandler?(response) + self.handleProviderMessage(message, completionHandler: completionHandler) } catch { self.providerLogger.error( error: error, - message: "Failed to handle app message." + message: "Failed to decode app message." ) completionHandler?(nil) @@ -81,15 +82,26 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } } - private func processMessage(_ messageData: Data) throws -> Data? { - let message = try TunnelProviderMessage(messageData: messageData) - + private func handleProviderMessage( + _ message: TunnelProviderMessage, + completionHandler: ((Data?) -> Void)? + ) { switch message { case .getTunnelStatus: var tunnelStatus = PacketTunnelStatus() tunnelStatus.tunnelRelay = self.selectorResult?.packetTunnelRelay - return try TunnelProviderReply(tunnelStatus).encode() + var reply: Data? + do { + reply = try TunnelProviderReply(tunnelStatus).encode() + } catch { + self.providerLogger.error( + error: error, + message: "Failed to encode tunnel status." + ) + } + + completionHandler?(reply) case let .reconnectTunnel(aSelectorResult): reasserting = true @@ -97,8 +109,45 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { selectorResult = aSelectorResult } reasserting = false + completionHandler?(nil) + + case let .sendURLRequest(proxyRequest): + let task = REST.sharedURLSession + .dataTask(with: proxyRequest.urlRequest) { [weak self] data, response, error in + guard let self = self else { return } + + self.dispatchQueue.async { + self.proxiedRequests.removeValue(forKey: proxyRequest.id) + + var reply: Data? + do { + let proxyResponse = ProxyURLResponse( + data: data, + response: response, + error: error + ) + reply = try TunnelProviderReply(proxyResponse).encode() + } catch { + self.providerLogger.error( + error: error, + message: "Failed to encode ProxyURLResponse." + ) + } + + completionHandler?(reply) + } + } + + proxiedRequests[proxyRequest.id] = task + + task.resume() + + case let .cancelURLRequest(id): + let task = proxiedRequests.removeValue(forKey: id) + + task?.cancel() - return nil + completionHandler?(nil) } } diff --git a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift new file mode 100644 index 0000000000..121e6eac5e --- /dev/null +++ b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift @@ -0,0 +1,39 @@ +// +// PacketTunnelTransport.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-10-03. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +final class PacketTunnelTransport: RESTTransport { + var name: String { + return "packet-tunnel" + } + + func sendRequest( + _ request: URLRequest, + completion: @escaping (Data?, URLResponse?, Error?) -> Void + ) throws -> Cancellable { + let proxyRequest = try ProxyURLRequest(id: UUID(), urlRequest: request) + + return try TunnelManager.shared.sendRequest(proxyRequest) { result in + switch result { + case .cancelled: + completion(nil, nil, URLError(.cancelled)) + + case let .success(reply): + completion( + reply.data, + reply.response?.originalResponse, + reply.error?.originalError + ) + + case let .failure(error): + completion(nil, nil, error) + } + } + } +} diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift new file mode 100644 index 0000000000..4ec4f4a92a --- /dev/null +++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift @@ -0,0 +1,83 @@ +// +// TransportMonitor.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-10-07. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class TransportMonitor: TunnelObserver { + private let packetTunnelTransport = PacketTunnelTransport() + private let urlSessionTransport = URLSessionTransport(urlSession: REST.sharedURLSession) + + init() { + TunnelManager.shared.addObserver(self) + + setTransports() + } + + // MARK: - TunnelObserver + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { + setTransports() + } + + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + setTransports() + } + + func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { + setTransports() + } + + func tunnelManager( + _ manager: TunnelManager, + didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 + ) {} + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {} + + // MARK: - Private + + private func setTransports() { + RESTTransportRegistry.shared.setTransport( + stateUpdated( + tunnelState: TunnelManager.shared.tunnelStatus.state, + deviceState: TunnelManager.shared.deviceState + ) + ) + } + + private func stateUpdated( + tunnelState: TunnelState, + deviceState: DeviceState + ) -> RESTTransport { + switch (tunnelState, deviceState) { + case (.connected, .revoked): + return packetTunnelTransport + + case (.pendingReconnect, _): + return urlSessionTransport + + case (.waitingForConnectivity, _): + return urlSessionTransport + + case (.connecting, _): + return packetTunnelTransport + + case (.reconnecting, _): + return packetTunnelTransport + + case (.disconnecting, _): + return urlSessionTransport + + case (.disconnected, _): + return urlSessionTransport + + case (.connected, _): + return urlSessionTransport + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/ProxyURLRequest.swift b/ios/MullvadVPN/TunnelManager/ProxyURLRequest.swift new file mode 100644 index 0000000000..ccc3ec4ee7 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/ProxyURLRequest.swift @@ -0,0 +1,100 @@ +// +// ProxyURLRequest.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-10-03. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Struct describing serializable URLRequest data. +struct ProxyURLRequest: Codable { + let id: UUID + let url: URL + let method: String? + let httpBody: Data? + let httpHeaders: [String: String]? + + var urlRequest: URLRequest { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = method + urlRequest.httpBody = httpBody + urlRequest.allHTTPHeaderFields = httpHeaders + return urlRequest + } + + init(id: UUID, urlRequest: URLRequest) throws { + guard let url = urlRequest.url else { + throw InvalidURLRequestError() + } + + self.id = id + self.url = url + method = urlRequest.httpMethod + httpBody = urlRequest.httpBody + httpHeaders = urlRequest.allHTTPHeaderFields + } +} + +/// Struct describing serializable URLResponse data. +struct ProxyURLResponse: Codable { + let data: Data? + let response: HTTPURLResponseWrapper? + let error: URLErrorWrapper? + + init(data: Data?, response: URLResponse?, error: Error?) { + self.data = data + self.response = response.flatMap { HTTPURLResponseWrapper($0) } + self.error = error.flatMap { URLErrorWrapper($0) } + } +} + +struct URLErrorWrapper: Codable { + let code: Int? + let localizedDescription: String + + init?(_ error: Error) { + localizedDescription = error.localizedDescription + code = (error as? URLError)?.errorCode + } + + var originalError: Error? { + guard let code = code else { return nil } + + return URLError(URLError.Code(rawValue: code)) + } +} + +struct HTTPURLResponseWrapper: Codable { + let url: URL? + let statusCode: Int + let headerFields: [String: String]? + + init?(_ response: URLResponse) { + guard let response = response as? HTTPURLResponse else { return nil } + + url = response.url + statusCode = response.statusCode + headerFields = Dictionary( + uniqueKeysWithValues: response.allHeaderFields.map { ("\($0)", "\($1)") } + ) + } + + var originalResponse: HTTPURLResponse? { + guard let url = url else { return nil } + + return HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: nil, + headerFields: headerFields + ) + } +} + +struct InvalidURLRequestError: LocalizedError { + var errorDescription: String? { + return "Invalid URLRequest URL." + } +} diff --git a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift index 04febe26a8..5a31cbd7d1 100644 --- a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift @@ -10,21 +10,21 @@ import Foundation import NetworkExtension import Operations -private enum MessagingConfiguration { - /// Delay for sending tunnel provider messages to the tunnel when in connecting state. - /// Used to workaround a bug when talking to the tunnel too early during startup may cause it - /// to freeze. - static let connectingStateWaitDelay: TimeInterval = 5 +/// Delay for sending tunnel provider messages to the tunnel when in connecting state. +/// Used to workaround a bug when talking to the tunnel too early during startup may cause it +/// to freeze. +private let connectingStateWaitDelay: TimeInterval = 5 - /// Timeout interval in seconds. - static let timeout: TimeInterval = 5 -} +/// Default timeout in seconds. +private let defaultTimeout: TimeInterval = 5 final class SendTunnelProviderMessageOperation<Output>: ResultOperation<Output, Error> { typealias DecoderHandler = (Data?) throws -> Output private let tunnel: Tunnel private let message: TunnelProviderMessage + private let timeout: TimeInterval + private let decoderHandler: DecoderHandler private var statusObserver: TunnelStatusBlockObserver? @@ -37,11 +37,14 @@ final class SendTunnelProviderMessageOperation<Output>: ResultOperation<Output, dispatchQueue: DispatchQueue, tunnel: Tunnel, message: TunnelProviderMessage, + timeout: TimeInterval? = nil, decoderHandler: @escaping DecoderHandler, - completionHandler: @escaping CompletionHandler + completionHandler: CompletionHandler? ) { self.tunnel = tunnel self.message = message + self.timeout = timeout ?? defaultTimeout + self.decoderHandler = decoderHandler super.init( @@ -96,7 +99,7 @@ final class SendTunnelProviderMessageOperation<Output>: ResultOperation<Output, timeoutWork = workItem // Schedule timeout work. - let deadline: DispatchWallTime = .now() + MessagingConfiguration.timeout + delay + let deadline: DispatchWallTime = .now() + timeout + delay dispatchQueue.asyncAfter(wallDeadline: deadline, execute: workItem) } @@ -140,12 +143,12 @@ final class SendTunnelProviderMessageOperation<Output>: ResultOperation<Output, waitForConnectingStateWork = nil // Execute right away if enough time passed since the tunnel was launched. - guard timeElapsed < MessagingConfiguration.connectingStateWaitDelay else { + guard timeElapsed < connectingStateWaitDelay else { block() return } - let waitDelay = MessagingConfiguration.connectingStateWaitDelay - timeElapsed + let waitDelay = connectingStateWaitDelay - timeElapsed let workItem = DispatchWorkItem(block: block) // Assign new work. @@ -201,12 +204,14 @@ extension SendTunnelProviderMessageOperation where Output: Codable { dispatchQueue: DispatchQueue, tunnel: Tunnel, message: TunnelProviderMessage, + timeout: TimeInterval? = nil, completionHandler: @escaping CompletionHandler ) { self.init( dispatchQueue: dispatchQueue, tunnel: tunnel, message: message, + timeout: timeout, decoderHandler: { data in if let data = data { return try TunnelProviderReply(messageData: data).value @@ -224,12 +229,14 @@ extension SendTunnelProviderMessageOperation where Output == Void { dispatchQueue: DispatchQueue, tunnel: Tunnel, message: TunnelProviderMessage, - completionHandler: @escaping CompletionHandler + timeout: TimeInterval? = nil, + completionHandler: CompletionHandler? ) { self.init( dispatchQueue: dispatchQueue, tunnel: tunnel, message: message, + timeout: timeout, decoderHandler: { _ in () }, completionHandler: completionHandler ) diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift index ef2f4de6e2..c01ea3a4d1 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift @@ -15,6 +15,10 @@ private let operationQueue = AsyncOperationQueue() /// Shared queue used by IPC operations. private let dispatchQueue = DispatchQueue(label: "Tunnel.dispatchQueue") +/// Timeout for proxy requests. +private let proxyRequestTimeout: TimeInterval = ApplicationConfiguration + .defaultAPINetworkTimeout + 2 + extension Tunnel { /// Request packet tunnel process to reconnect the tunnel with the given relay selector result. /// Packet tunnel will reconnect to the current relay if relay selector result is not provided. @@ -49,4 +53,37 @@ extension Tunnel { return operation } + + /// Send HTTP request via packet tunnel process bypassing VPN. + func sendRequest( + _ proxyRequest: ProxyURLRequest, + completionHandler: @escaping (OperationCompletion<ProxyURLResponse, Error>) -> Void + ) -> Cancellable { + let operation = SendTunnelProviderMessageOperation( + dispatchQueue: dispatchQueue, + tunnel: self, + message: .sendURLRequest(proxyRequest), + timeout: proxyRequestTimeout, + completionHandler: completionHandler + ) + + operation.addBlockObserver( + OperationBlockObserver(didCancel: { [weak self] _ in + guard let self = self else { return } + + let cancelOperation = SendTunnelProviderMessageOperation( + dispatchQueue: dispatchQueue, + tunnel: self, + message: .cancelURLRequest(proxyRequest.id), + completionHandler: nil + ) + + operationQueue.addOperation(cancelOperation) + }) + ) + + operationQueue.addOperation(operation) + + return operation + } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index dbe10f2056..04b55ff204 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -513,6 +513,20 @@ final class TunnelManager { ) } + /// Send URL request via packet tunnel process bypassing VPN. + /// This function is primarily used by `PacketTunnelTransport` to go outside of VPN when the + /// tunnel is broken. + func sendRequest( + _ proxyRequest: ProxyURLRequest, + completionHandler: @escaping (OperationCompletion<ProxyURLResponse, Error>) -> Void + ) throws -> Cancellable { + if let tunnel { + return tunnel.sendRequest(proxyRequest, completionHandler: completionHandler) + } else { + throw UnsetTunnelError() + } + } + // MARK: - Tunnel observeration /// Add tunnel observer. diff --git a/ios/MullvadVPN/TunnelManager/TunnelProviderMessage.swift b/ios/MullvadVPN/TunnelManager/TunnelProviderMessage.swift index a534e577b8..836d79c94a 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelProviderMessage.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelProviderMessage.swift @@ -17,12 +17,22 @@ enum TunnelProviderMessage: Codable, CustomStringConvertible { /// Request the tunnel status. case getTunnelStatus + /// Send HTTP request outside of VPN tunnel. + case sendURLRequest(ProxyURLRequest) + + /// Cancel HTTP request sent outside of VPN tunnel. + case cancelURLRequest(UUID) + var description: String { switch self { case .reconnectTunnel: return "reconnect-tunnel" case .getTunnelStatus: return "get-tunnel-status" + case .sendURLRequest: + return "send-http-request" + case .cancelURLRequest: + return "cancel-http-request" } } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 0386279ac7..1c896c5018 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -38,6 +38,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Current selector result. private var selectorResult: RelaySelectorResult? + /// List of all proxied network requests bypassing VPN. + private var proxiedRequests: [UUID: URLSessionDataTask] = [:] + /// A system completion handler passed from startTunnel and saved for later use once the /// connection is established. private var startTunnelCompletionHandler: (() -> Void)? @@ -237,6 +240,43 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } completionHandler?(response) + + case let .sendURLRequest(proxyRequest): + let task = REST.sharedURLSession.dataTask( + with: proxyRequest.urlRequest + ) { [weak self] data, response, error in + guard let self = self else { return } + + self.dispatchQueue.async { + self.proxiedRequests.removeValue(forKey: proxyRequest.id) + + var reply: Data? + do { + let response = ProxyURLResponse( + data: data, + response: response, + error: error + ) + reply = try TunnelProviderReply(response).encode() + } catch { + self.providerLogger.error( + error: error, + message: "Failed to encode ProxyURLResponse." + ) + } + + completionHandler?(reply) + } + } + + self.proxiedRequests[proxyRequest.id] = task + + task.resume() + + case let .cancelURLRequest(id): + let task = self.proxiedRequests.removeValue(forKey: id) + + task?.cancel() } } } |
