summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-10-13 10:03:31 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-10-13 10:03:31 +0200
commitca62e306d472a9d8d2dd742df6d7c5b5b7d91097 (patch)
tree8c8b762962cafa41f9d809639d0d0c9820ee5009
parent512eb4d1949b1979d6a8a95e44f506de3a7b654b (diff)
parent7696ee36e26f1d58205bea6d49b826ab8b95651e (diff)
downloadmullvadvpn-ca62e306d472a9d8d2dd742df6d7c5b5b7d91097.tar.xz
mullvadvpn-ca62e306d472a9d8d2dd742df6d7c5b5b7d91097.zip
Merge branch 'packet-tunnel-transport-for-HTTP(s)-requests'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj42
-rw-r--r--ios/MullvadVPN/AppDelegate.swift2
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift7
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift25
-rw-r--r--ios/MullvadVPN/REST/RESTNetworkOperation.swift163
-rw-r--r--ios/MullvadVPN/REST/RESTProxy.swift8
-rw-r--r--ios/MullvadVPN/REST/RESTProxyFactory.swift2
-rw-r--r--ios/MullvadVPN/REST/RESTTransport.swift18
-rw-r--r--ios/MullvadVPN/REST/RESTTransportRegistry.swift32
-rw-r--r--ios/MullvadVPN/REST/URLSessionTransport.swift32
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift65
-rw-r--r--ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift39
-rw-r--r--ios/MullvadVPN/TransportMonitor/TransportMonitor.swift83
-rw-r--r--ios/MullvadVPN/TunnelManager/ProxyURLRequest.swift100
-rw-r--r--ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift33
-rw-r--r--ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift37
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift14
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelProviderMessage.swift10
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift40
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()
}
}
}