diff options
| author | Emīls <emils@mullvad.net> | 2023-04-19 11:08:52 +0100 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2023-05-22 10:18:27 +0200 |
| commit | e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2 (patch) | |
| tree | 4e0c6cf3336c69c1c97b0640d5c06bc27819594c /ios | |
| parent | 77f51e690b26346ff4b251c27eb2ece493820d85 (diff) | |
| download | mullvadvpn-e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2.tar.xz mullvadvpn-e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2.zip | |
Add shadowsocks-proxy crate
Diffstat (limited to 'ios')
33 files changed, 1041 insertions, 132 deletions
diff --git a/ios/MullvadREST/RESTNetworkOperation.swift b/ios/MullvadREST/RESTNetworkOperation.swift index 6f723e5865..4b3705ce76 100644 --- a/ios/MullvadREST/RESTNetworkOperation.swift +++ b/ios/MullvadREST/RESTNetworkOperation.swift @@ -17,7 +17,7 @@ extension REST { private let responseHandler: AnyResponseHandler<Success> private let logger: Logger - private let transportProvider: () -> RESTTransport? + private let transportProvider: () -> RESTTransportProvider? private let addressCacheStore: AddressCache private var networkTask: Cancellable? @@ -30,6 +30,7 @@ extension REST { private var retryDelayIterator: AnyIterator<Duration> private var retryTimer: DispatchSourceTimer? private var retryCount = 0 + private var transportStrategy = TransportStrategy() init( name: String, @@ -140,7 +141,13 @@ extension REST { private func didReceiveURLRequest(_ restRequest: REST.Request, endpoint: AnyIPEndpoint) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) - guard let transport = transportProvider() else { + let suggestedTransport = transportStrategy.connectionTransport() + let transportProvider = transportProvider() + let transport = suggestedTransport == .useShadowSocks + ? transportProvider?.shadowSocksTransport() + : transportProvider?.transport() + + guard let transport = transport else { logger.error("Failed to obtain transport.") finish(result: .failure(REST.Error.transport(NoTransportError()))) return @@ -153,33 +160,27 @@ extension REST { """ ) - do { - networkTask = try transport - .sendRequest(restRequest.urlRequest) { [weak self] data, response, error in - guard let self = self else { return } - - self.dispatchQueue.async { - if let error = error { - self.didReceiveError( - error, - transport: transport, - endpoint: endpoint - ) - } else { - let httpResponse = response as! HTTPURLResponse - let data = data ?? Data() + networkTask = transport.sendRequest(restRequest.urlRequest) { [weak self] data, response, error in + guard let self = self else { return } + 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, - transport: transport, - data: data, - endpoint: endpoint - ) - } - } + self.didReceiveURLResponse( + httpResponse, + transport: transport, + data: data, + endpoint: endpoint + ) } - } catch { - didReceiveError(error, transport: transport, endpoint: endpoint) + } } } @@ -213,6 +214,7 @@ extension REST { default: if !REST.isStagingEnvironment { _ = addressCacheStore.selectNextEndpoint(endpoint) + transportStrategy.didFail() } } } diff --git a/ios/MullvadREST/RESTProxy.swift b/ios/MullvadREST/RESTProxy.swift index 5c8a14df81..b252d2e33f 100644 --- a/ios/MullvadREST/RESTProxy.swift +++ b/ios/MullvadREST/RESTProxy.swift @@ -67,11 +67,11 @@ extension REST { } public class ProxyConfiguration { - public let transportProvider: () -> RESTTransport? + public let transportProvider: () -> RESTTransportProvider? public let addressCacheStore: AddressCache public init( - transportProvider: @escaping () -> RESTTransport?, + transportProvider: @escaping () -> RESTTransportProvider?, addressCacheStore: AddressCache ) { self.transportProvider = transportProvider diff --git a/ios/MullvadREST/RESTProxyFactory.swift b/ios/MullvadREST/RESTProxyFactory.swift index 6bc793a722..8d2bba41d3 100644 --- a/ios/MullvadREST/RESTProxyFactory.swift +++ b/ios/MullvadREST/RESTProxyFactory.swift @@ -13,7 +13,7 @@ extension REST { public let configuration: AuthProxyConfiguration public class func makeProxyFactory( - transportProvider: @escaping () -> RESTTransport?, + transportProvider: @escaping () -> RESTTransportProvider?, addressCache: AddressCache ) -> ProxyFactory { let basicConfiguration = REST.ProxyConfiguration( diff --git a/ios/MullvadREST/RESTTransport.swift b/ios/MullvadREST/RESTTransport.swift index c8a1aa5b36..b739828bf2 100644 --- a/ios/MullvadREST/RESTTransport.swift +++ b/ios/MullvadREST/RESTTransport.swift @@ -15,5 +15,15 @@ public protocol RESTTransport { func sendRequest( _ request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void - ) throws -> Cancellable + ) -> Cancellable +} + +public protocol RESTTransportProvider { + /// Requests a new transport + /// - Returns: A transport layer + func transport() -> RESTTransport? + + /// Requests a Shadowsocks transport + /// - Returns: A transport layer that proxies the requests to a local Shadowsocks proxy instance + func shadowSocksTransport() -> RESTTransport? } diff --git a/ios/MullvadREST/RESTTransportStrategy.swift b/ios/MullvadREST/RESTTransportStrategy.swift new file mode 100644 index 0000000000..5162100a64 --- /dev/null +++ b/ios/MullvadREST/RESTTransportStrategy.swift @@ -0,0 +1,47 @@ +// +// RESTTransportStrategy.swift +// MullvadREST +// +// Created by Marco Nikic on 2023-04-27. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension REST { + public struct TransportStrategy: Codable { + /// The different transports suggested by the strategy + public enum Transport { + /// Suggests using a direct connection + case useURLSession + /// Suggests connecting via Shadowsocks proxy + case useShadowSocks + } + + /// The internal counter for suggested transports. + /// + /// A value of `0` means a direct transport suggestion, a value of `1` or `2` means a Shadowsocks transport + /// suggestion. + private var connectionAttempts: UInt + + public init() { + connectionAttempts = 0 + } + + /// Instructs the strategy that a network connection failed + /// + /// Every third failure results in a direct transport suggestion. + public mutating func didFail() { + connectionAttempts += 1 + // Avoid overflowing by resetting back to 0 every 3rd failure + connectionAttempts = connectionAttempts.isMultiple(of: 3) ? 0 : connectionAttempts + } + + /// The suggested connection transport + /// + /// - Returns: `.useURLSession` for every 3rd failed attempt, `.useShadowSocks` otherwise + public func connectionTransport() -> Transport { + connectionAttempts.isMultiple(of: 3) ? .useURLSession : .useShadowSocks + } + } +} diff --git a/ios/MullvadREST/RESTURLSession.swift b/ios/MullvadREST/RESTURLSession.swift index 87e2d7abbe..99bafcee3c 100644 --- a/ios/MullvadREST/RESTURLSession.swift +++ b/ios/MullvadREST/RESTURLSession.swift @@ -9,27 +9,7 @@ import Foundation extension REST { - public struct HTTPProxyConfiguration { - public var address: String - public var port: UInt16 - - public init(address: String, port: UInt16) { - self.address = address - self.port = port - } - - fileprivate func apply(to sessionConfiguration: URLSessionConfiguration) { - var configuration = [CFString: Any]() - - configuration[kCFNetworkProxiesHTTPProxy] = address - configuration[kCFNetworkProxiesHTTPPort] = NSNumber(value: port) - configuration[kCFNetworkProxiesProxyAutoConfigEnable] = kCFBooleanFalse - - sessionConfiguration.connectionProxyDictionary = configuration - } - } - - public static func makeURLSession(httpProxyConfiguration: HTTPProxyConfiguration? = nil) -> URLSession { + public static func makeURLSession() -> URLSession { let certificatePath = Bundle(for: SSLPinningURLSessionDelegate.self) .path(forResource: "le_root_cert", ofType: "cer")! let data = FileManager.default.contents(atPath: certificatePath)! @@ -41,7 +21,6 @@ extension REST { ) let sessionConfiguration = URLSessionConfiguration.ephemeral - httpProxyConfiguration?.apply(to: sessionConfiguration) let session = URLSession( configuration: sessionConfiguration, diff --git a/ios/MullvadREST/ServerRelaysResponse.swift b/ios/MullvadREST/ServerRelaysResponse.swift index d6e8b454a2..d91d05baee 100644 --- a/ios/MullvadREST/ServerRelaysResponse.swift +++ b/ios/MullvadREST/ServerRelaysResponse.swift @@ -26,6 +26,17 @@ extension REST { } } + public struct BridgeRelay: Codable { + public let hostname: String + public let active: Bool + public let owned: Bool + public let location: String + public let provider: String + public let ipv4AddrIn: IPv4Address + public let weight: UInt64 + public let includeInCountry: Bool + } + public struct ServerRelay: Codable { public let hostname: String public let active: Bool @@ -98,9 +109,11 @@ extension REST { public struct ServerBridges: Codable { public let shadowsocks: [ServerShadowsocks] + public let relays: [BridgeRelay] - public init(shadowsocks: [REST.ServerShadowsocks]) { + public init(shadowsocks: [REST.ServerShadowsocks], relays: [BridgeRelay]) { self.shadowsocks = shadowsocks + self.relays = relays } } diff --git a/ios/MullvadREST/ShadowSocksProxy.swift b/ios/MullvadREST/ShadowSocksProxy.swift new file mode 100644 index 0000000000..a905cdc9c7 --- /dev/null +++ b/ios/MullvadREST/ShadowSocksProxy.swift @@ -0,0 +1,83 @@ +// +// ShadowSocksProxy.swift +// MullvadREST +// +// Created by Emils on 19/04/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Network +import Shadowsocks + +public class ShadowSocksProxy { + private var proxyConfig: ProxyHandle + private let remoteAddress: IPAddress + private let remotePort: UInt16 + private let password: String + private let cipher: String + private var didStart = false + private let stateLock = NSLock() + + public init(remoteAddress: IPAddress, remotePort: UInt16, password: String, cipher: String) { + proxyConfig = ProxyHandle(context: nil, port: 0) + self.remoteAddress = remoteAddress + self.remotePort = remotePort + self.password = password + self.cipher = cipher + } + + /// The local port for the shadow socks proxy + /// + /// - Returns: The local port for the shadow socks proxy when it has started, 0 otherwise. + public func localPort() -> UInt16 { + stateLock.lock() + defer { stateLock.unlock() } + return proxyConfig.port + } + + deinit { + stop() + } + + /// Starts the socks proxy + public func start() { + stateLock.lock() + defer { stateLock.unlock() } + guard didStart == false else { return } + didStart = true + + // Get the raw bytes of `addr.rawValue` + remoteAddress.rawValue.withUnsafeBytes { unsafeAddressPointer in + + // Rebind the raw bytes to an array of bytes, and get a pointer to its beginning + let rawAddr = unsafeAddressPointer.bindMemory(to: UInt8.self).baseAddress + + // Get the raw bytes access to `proxyConfig` + _ = withUnsafeMutablePointer(to: &proxyConfig) { config in + start_shadowsocks_proxy( + rawAddr, + UInt(remoteAddress.rawValue.count), + remotePort, + password, + UInt(password.count), + cipher, + UInt(cipher.count), + config + ) + } + } + } + + /// Stops the socks proxy + public func stop() { + stateLock.lock() + defer { stateLock.unlock() } + guard didStart == true else { return } + didStart = false + + _ = withUnsafePointer(to: proxyConfig) { pointer in + stop_shadowsocks_proxy(UnsafeMutablePointer(mutating: pointer)) + } + } +} diff --git a/ios/MullvadREST/URLSessionTransport.swift b/ios/MullvadREST/URLSessionTransport.swift index 4acc155d58..0bb765c686 100644 --- a/ios/MullvadREST/URLSessionTransport.swift +++ b/ios/MullvadREST/URLSessionTransport.swift @@ -26,10 +26,54 @@ extension REST { public func sendRequest( _ request: URLRequest, completion: @escaping (Data?, URLResponse?, Swift.Error?) -> Void - ) throws -> Cancellable { + ) -> Cancellable { let dataTask = urlSession.dataTask(with: request, completionHandler: completion) dataTask.resume() return dataTask } } + + public final class URLSessionShadowSocksTransport: RESTTransport { + public var name: String { + return "shadow-socks-url-session" + } + + public let urlSession: URLSession + private let shadowSocksProxy: ShadowSocksProxy + + public init( + urlSession: URLSession, + shadowSocksConfiguration: ServerShadowsocks, + shadowSocksBridgeRelay: BridgeRelay + ) { + self.urlSession = urlSession + shadowSocksProxy = ShadowSocksProxy( + remoteAddress: shadowSocksBridgeRelay.ipv4AddrIn, + remotePort: shadowSocksConfiguration.port, + password: shadowSocksConfiguration.password, + cipher: shadowSocksConfiguration.cipher + ) + } + + public func sendRequest( + _ request: URLRequest, + completion: @escaping (Data?, URLResponse?, Swift.Error?) -> Void + ) -> Cancellable { + // Start the shadow socks proxy in order to get a local port + shadowSocksProxy.start() + + // Copy the URL request and rewrite the host and port to point to the shadow socks proxy instance + var urlRequestCopy = request + urlRequestCopy.url = request.url.flatMap { url in + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.host = "127.0.0.1" + components?.port = Int(shadowSocksProxy.localPort()) + return components?.url + } + + let dataTask = urlSession.dataTask(with: urlRequestCopy, completionHandler: completion) + dataTask.resume() + return dataTask + } + } } diff --git a/ios/MullvadREST/module.private.modulemap b/ios/MullvadREST/module.private.modulemap new file mode 100644 index 0000000000..ed8bb99f2e --- /dev/null +++ b/ios/MullvadREST/module.private.modulemap @@ -0,0 +1,5 @@ +framework module Shadowsocks { + header "shadowsocks.h" + link "libshadowsocks_proxy" + export * +} diff --git a/ios/MullvadREST/shadowsocks-proxy/.gitignore b/ios/MullvadREST/shadowsocks-proxy/.gitignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/ios/MullvadREST/shadowsocks-proxy/Cargo.toml b/ios/MullvadREST/shadowsocks-proxy/Cargo.toml new file mode 100644 index 0000000000..364952f73c --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "shadowsocks-proxy" +version = "0.0.0" +edition = "2021" +license = "GPL-3.0" +publish = false + +[lib] +crate-type = [ "rlib", "staticlib" ] +bench = false + +[dependencies] +shadowsocks-service.git = "https://github.com/mullvad/shadowsocks-rust" +shadowsocks-service.rev = "8f6afd081a9440fff2dda565908eddc27a3295f1" +shadowsocks-service.features = [ "local", "stream-cipher", "local-http", "local-tunnel" ] + +tokio = "1" +libc = "0.2" +log = "0.4" + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +oslog = "0.2" + +[target.'cfg(target_os = "ios")'.build-dependencies] +cbindgen = "0.24" diff --git a/ios/MullvadREST/shadowsocks-proxy/build.rs b/ios/MullvadREST/shadowsocks-proxy/build.rs new file mode 100644 index 0000000000..3a0ffb572b --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/build.rs @@ -0,0 +1,14 @@ +#[cfg(target_os = "ios")] +fn main() { + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_language(cbindgen::Language::C) + .generate() + .expect("failed to generate bindings") + .write_to_file("include/shadowsocks.h"); +} + +#[cfg(not(target_os = "ios"))] +fn main() {} diff --git a/ios/MullvadREST/shadowsocks-proxy/build.sh b/ios/MullvadREST/shadowsocks-proxy/build.sh new file mode 100644 index 0000000000..7617bc91c8 --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/build.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +if [ "$#" -ne 1 ] +then + echo "Usage (note: only call inside xcode!):" + echo "compile-library.sh <FFI_TARGET> <buildvariant>" + exit 1 +fi + +# what to pass to cargo build -p, e.g. your_lib_ffi +FFI_TARGET=$1 + +RELFLAG= +if [[ "$CONFIGURATION" -eq "Release" ]]; then + RELFLAG=--release +fi + +set -euvx + +if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then + # Assume we're in Xcode, which means we're probably cross-compiling. + # In this case, we need to add an extra library search path for build scripts and proc-macros, + # which run on the host instead of the target. + # (macOS Big Sur does not have linkable libraries in /usr/lib/.) + export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}" +fi + +IS_SIMULATOR=0 +if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then + IS_SIMULATOR=1 +fi + +for arch in $ARCHS; do + case "$arch" in + x86_64) + if [ $IS_SIMULATOR -eq 0 ]; then + echo "Building for x86_64, but not a simulator build. What's going on?" >&2 + exit 2 + fi + + # Intel iOS simulator + export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios" + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib $RELFLAG --target x86_64-apple-ios + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib --target x86_64-apple-ios + ;; + + arm64) + if [ $IS_SIMULATOR -eq 0 ]; then + # Hardware iOS targets + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib $RELFLAG --target aarch64-apple-ios + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib --target aarch64-apple-ios + else + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib $RELFLAG --target aarch64-apple-ios-sim + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib --target aarch64-apple-ios-sim + fi + esac +done diff --git a/ios/MullvadREST/shadowsocks-proxy/include/shadowsocks.h b/ios/MullvadREST/shadowsocks-proxy/include/shadowsocks.h new file mode 100644 index 0000000000..eecb240d67 --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/include/shadowsocks.h @@ -0,0 +1,20 @@ +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +typedef struct ProxyHandle { + void *context; + uint16_t port; +} ProxyHandle; + +int32_t start_shadowsocks_proxy(const uint8_t *addr, + uintptr_t addr_len, + uint16_t port, + const uint8_t *password, + uintptr_t password_len, + const uint8_t *cipher, + uintptr_t cipher_len, + struct ProxyHandle *proxy_config); + +int32_t stop_shadowsocks_proxy(struct ProxyHandle *proxy_config); diff --git a/ios/MullvadREST/shadowsocks-proxy/src/bin/run.rs b/ios/MullvadREST/shadowsocks-proxy/src/bin/run.rs new file mode 100644 index 0000000000..073bd9cc02 --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/src/bin/run.rs @@ -0,0 +1,17 @@ +use std::{net::SocketAddr, str::FromStr}; + +fn main() { + let socketaddr = SocketAddr::from_str("185.65.135.117:443").unwrap(); + let password = "mullvad"; + let cipher = "aes-256-gcm"; + + let (port, handle) = shadowsocks_proxy::run_forwarding_proxy(socketaddr, password, cipher) + .expect("failed to start SOCKS proxy"); + + println!("Running proxy on port {port}"); + + let _ = std::io::stdin().read_line(&mut String::new()); + println!("Stopping proxy"); + handle.stop(); + println!("Done"); +} diff --git a/ios/MullvadREST/shadowsocks-proxy/src/bin/run_unsafe.rs b/ios/MullvadREST/shadowsocks-proxy/src/bin/run_unsafe.rs new file mode 100644 index 0000000000..1a3be6d541 --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/src/bin/run_unsafe.rs @@ -0,0 +1,51 @@ +use std::{ + net::{Ipv4Addr, SocketAddr}, + str::FromStr, +}; + +fn main() { + let socketaddr = SocketAddr::from_str("185.65.135.117:443").unwrap(); + let password = "mullvad"; + let cipher = "aes-256-gcm"; + + let cipher_ptr = cipher.as_ptr(); + let cipher_size = cipher.as_bytes().len(); + + let password_ptr = password.as_ptr(); + let password_size = password.as_bytes().len(); + + let addr = Ipv4Addr::from_str("185.65.135.117").unwrap(); + let addr_bytes = addr.octets(); + let addr_ptr = addr_bytes.as_ptr(); + + let mut ctx = shadowsocks_proxy::ProxyHandle { + port: 0, + context: std::ptr::null_mut(), + }; + + let retval = unsafe { + shadowsocks_proxy::start_shadowsocks_proxy( + addr_ptr, + addr_bytes.len(), + socketaddr.port(), + password_ptr, + password_size, + cipher_ptr, + cipher_size, + &mut ctx as *mut _, + ) + }; + if retval != 0 { + println!("Failed to start proxy - {retval}"); + return; + } + + println!("Running proxy on port {}", ctx.port); + let _ = std::io::stdin().read_line(&mut String::new()); + println!("Stopping proxy"); + let retval = unsafe { shadowsocks_proxy::stop_shadowsocks_proxy(&mut ctx as *mut _) }; + if retval != 0 { + println!("Failed to stop proxy"); + } + println!("Done"); +} diff --git a/ios/MullvadREST/shadowsocks-proxy/src/ffi.rs b/ios/MullvadREST/shadowsocks-proxy/src/ffi.rs new file mode 100644 index 0000000000..d8c575ef7e --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/src/ffi.rs @@ -0,0 +1,128 @@ +use super::{run_forwarding_proxy, ShadowsocksHandle}; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use std::sync::Once; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +static INIT_LOGGING: Once = Once::new(); + +#[repr(C)] +pub struct ProxyHandle { + pub context: *mut libc::c_void, + pub port: u16, +} + +/// # Safety +/// `addr`, `password`, `cipher` must be valid for the lifetime of this function call and they must +/// be backed by the amount of bytes as stored in the respective `*_len` parameters. +/// +/// `proxy_config` must be pointing to a valid memory region for the size of a `ProxyHandle` +/// instance. +#[no_mangle] +pub unsafe extern "C" fn start_shadowsocks_proxy( + addr: *const u8, + addr_len: usize, + port: u16, + password: *const u8, + password_len: usize, + cipher: *const u8, + cipher_len: usize, + proxy_config: *mut ProxyHandle, +) -> i32 { + #[cfg(any(target_os = "macos", target_os = "ios"))] + INIT_LOGGING.call_once(|| { + let _ = oslog::OsLogger::new("net.mullvad.MullvadVPN.ShadowSocks") + .level_filter(log::LevelFilter::Info) + .init(); + }); + + let bridge_ip = if let Some(addr) = unsafe { parse_ip_addr(addr, addr_len) } { + addr + } else { + return -1; + }; + + let bridge_addr = SocketAddr::new(bridge_ip, port); + + let password = if let Some(password) = unsafe { parse_str(password, password_len) } { + password + } else { + return -1; + }; + + let cipher = if let Some(cipher) = unsafe { parse_str(cipher, cipher_len) } { + cipher + } else { + return -1; + }; + + let (port, handle) = match run_forwarding_proxy(bridge_addr, &password, &cipher) { + Ok((port, handle)) => (port, handle), + Err(err) => { + log::error!("Failed to run HTTP proxy {}", err); + return err.raw_os_error().unwrap_or(-1); + } + }; + let handle = Box::new(handle); + + unsafe { + std::ptr::write( + proxy_config, + ProxyHandle { + port, + context: Box::into_raw(handle) as *mut _, + }, + ) + } + + 0 +} +/// # Safety +/// `proxy_config` must be pointing to a valid instance of a `ProxyInstance`, as instantiated by +/// `start_shadowsocks_proxy`. +#[no_mangle] +pub unsafe extern "C" fn stop_shadowsocks_proxy(proxy_config: *mut ProxyHandle) -> i32 { + let context_ptr = unsafe { (*proxy_config).context }; + if context_ptr.is_null() { + return -1; + } + + let proxy_handle: Box<ShadowsocksHandle> = unsafe { Box::from_raw(context_ptr as *mut _) }; + proxy_handle.stop(); + unsafe { (*proxy_config).context = std::ptr::null_mut() }; + 0 +} +/// Constructs a new IP address from a pointer containing bytes representing an IP address. +/// +/// SAFETY: `addr` must be a pointer to at least `addr_len` bytes. +unsafe fn parse_ip_addr(addr: *const u8, addr_len: usize) -> Option<IpAddr> { + match addr_len { + 4 => { + // SAFETY: addr pointer must point to at least addr_len bytes + let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; + Some(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]).into()) + } + 16 => { + // SAFETY: addr pointer must point to at least addr_len bytes + let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; + let mut addr_arr = [0u8; 16]; + addr_arr.as_mut_slice().copy_from_slice(bytes); + + Some(Ipv6Addr::from(addr_arr).into()) + } + anything_else => { + log::error!("Bad IP address length {anything_else}"); + None + } + } +} + +/// Allocates a new string with the contents of `data` if it contains only valid UTF-8 bytes. +/// +/// SAFETY: `data` must be a valid pointer to `len` amount of bytes +unsafe fn parse_str(data: *const u8, len: usize) -> Option<String> { + // SAFETY: data pointer must be valid for the size of len + let bytes = unsafe { std::slice::from_raw_parts(data, len) }; + String::from_utf8(bytes.to_vec()).ok() +} diff --git a/ios/MullvadREST/shadowsocks-proxy/src/lib.rs b/ios/MullvadREST/shadowsocks-proxy/src/lib.rs new file mode 100644 index 0000000000..66398cb9eb --- /dev/null +++ b/ios/MullvadREST/shadowsocks-proxy/src/lib.rs @@ -0,0 +1,160 @@ +use shadowsocks_service::{ + config::{ + Config, ConfigType, LocalConfig, LocalInstanceConfig, ProtocolType, ServerInstanceConfig, + }, + local::Server, + shadowsocks::{config::ServerConfig, crypto::CipherKind}, +}; +use std::{ + io, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, + str::FromStr, +}; +use tokio::sync::oneshot; + +mod ffi; +pub use ffi::{start_shadowsocks_proxy, stop_shadowsocks_proxy, ProxyHandle}; + +pub fn run_forwarding_proxy( + bridge_addr: SocketAddr, + password: &str, + cipher: &str, +) -> io::Result<(u16, ShadowsocksHandle)> { + let runtime = ShadowsocksRuntime::new(bridge_addr, password, cipher)?; + let port = runtime.port(); + let handle = runtime.run()?; + + Ok((port, handle)) +} + +struct ShadowsocksRuntime { + runtime: tokio::runtime::Runtime, + config: Config, + local_port: u16, +} + +pub struct ShadowsocksHandle { + tx: oneshot::Sender<oneshot::Sender<()>>, +} + +impl ShadowsocksHandle { + pub fn stop(self) { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let _ = self.tx.send(shutdown_tx); + let _ = shutdown_rx.blocking_recv(); + } +} + +impl ShadowsocksRuntime { + pub fn new(bridge_addr: SocketAddr, password: &str, cipher: &str) -> io::Result<Self> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let (config, local_port) = Self::create_config(bridge_addr, password, cipher)?; + Ok(Self { + runtime, + config, + local_port, + }) + } + + pub fn port(&self) -> u16 { + self.local_port + } + + pub fn run(self) -> io::Result<ShadowsocksHandle> { + let (tx, rx) = oneshot::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let (startup_tx, startup_rx) = oneshot::channel(); + std::thread::spawn(move || { + self.run_service_inner(rx, startup_tx); + }); + + match startup_rx.blocking_recv() { + Ok(Ok(_)) => Ok(ShadowsocksHandle { tx }), + Ok(Err(err)) => { + let _ = tx.send(shutdown_tx); + let _ = shutdown_rx.blocking_recv(); + Err(err) + } + Err(_) => { + let _ = tx.send(shutdown_tx); + let _ = shutdown_rx.blocking_recv(); + Err(io::Error::new( + io::ErrorKind::Other, + "Tokio runtime crashed", + )) + } + } + } + + fn run_service_inner( + self, + shutdown_rx: oneshot::Receiver<oneshot::Sender<()>>, + startup_done_tx: oneshot::Sender<io::Result<()>>, + ) { + let Self { + config, runtime, .. + } = self; + + std::thread::spawn(move || { + runtime.spawn(async move { + match Server::create(config).await { + Ok(server) => { + let _ = startup_done_tx.send(Ok(())); + let _ = server.wait_until_exit().await; + } + Err(err) => { + let _ = startup_done_tx.send(Err(err)); + } + } + }); + if let Ok(shutdown_tx) = runtime.block_on(shutdown_rx) { + std::mem::drop(runtime); + let _ = shutdown_tx.send(()); + } + }); + } + + pub fn create_config( + bridge_addr: SocketAddr, + password: &str, + cipher: &str, + ) -> io::Result<(Config, u16)> { + let mut cfg = Config::new(ConfigType::Local); + let free_port = get_free_port()?; + let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), free_port); + + let mut local_config = LocalConfig::new_with_addr(bind_addr.into(), ProtocolType::Tunnel); + local_config.forward_addr = + // TODO: Remove hardcoded API address + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 83, 223, 196)), 443).into()); + cfg.local = vec![LocalInstanceConfig::with_local_config(local_config)]; + + let cipher = match CipherKind::from_str(cipher) { + Ok(cipher) => cipher, + Err(err) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid cipher specified: {}", err), + )); + } + }; + let server_config = ServerInstanceConfig::with_server_config(ServerConfig::new( + bridge_addr, + password, + cipher, + )); + + cfg.server = vec![server_config]; + + Ok((cfg, free_port)) + } +} + +fn get_free_port() -> io::Result<u16> { + let bind_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + let port = TcpListener::bind(bind_addr)?.local_addr()?.port(); + Ok(port) +} diff --git a/ios/MullvadRESTTests/TransportStrategyTests.swift b/ios/MullvadRESTTests/TransportStrategyTests.swift new file mode 100644 index 0000000000..75ed74279d --- /dev/null +++ b/ios/MullvadRESTTests/TransportStrategyTests.swift @@ -0,0 +1,40 @@ +// +// TransportStrategyTests.swift +// MullvadRESTTests +// +// Created by Marco Nikic on 2023-04-27. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST +import XCTest + +final class TransportStrategyTests: XCTestCase { + func testEveryThirdConnectionAttemptsIsDirect() { + var strategy = REST.TransportStrategy() + + for index in 0 ... 12 { + let expectedResult: REST.TransportStrategy.Transport + expectedResult = index.isMultiple(of: 3) ? .useURLSession : .useShadowSocks + XCTAssertEqual(strategy.connectionTransport(), expectedResult) + strategy.didFail() + } + } + + func testLoadingFromCacheDoesNotImpactStrategy() throws { + var strategy = REST.TransportStrategy() + + // Fail twice, the next suggested transport mode should be via Shadowsocks proxy + strategy.didFail() + strategy.didFail() + XCTAssertEqual(strategy.connectionTransport(), .useShadowSocks) + + // Serialize the strategy and reload it from memory to simulate an application restart + let encodedRawStrategy = try JSONEncoder().encode(strategy) + var reloadedStrategy = try JSONDecoder().decode(REST.TransportStrategy.self, from: encodedRawStrategy) + + // This should be the third failure, the next suggested transport will be a direct one + reloadedStrategy.didFail() + XCTAssertEqual(reloadedStrategy.connectionTransport(), .useURLSession) + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 3bab2cdcf9..ae304ecf80 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 01F1FF1C29F06124007083C3 /* ShadowSocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F1FF1B29F06124007083C3 /* ShadowSocksProxy.swift */; }; + 01F1FF1E29F0627D007083C3 /* libshadowsocks_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */; }; 062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; }; 062B45AE28FD503000746E77 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 062B45AD28FD503000746E77 /* WireGuardKit */; }; 062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */; }; @@ -134,6 +136,7 @@ 586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; 586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; + 586F2BE229F6916F009E6924 /* shadowsocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 586F2BE129F6916F009E6924 /* shadowsocks.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871167E2910035700D41AAC /* PreferencesInteractor.swift */; }; 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; }; 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; }; @@ -369,6 +372,10 @@ 7AF0419E29E957EB00D492DD /* AccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */; }; A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; }; A9CF11FD2A0518E7001D9565 /* AddressCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */; }; + A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */; }; + A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; }; + A92CAAC629F7D33C008ED922 /* TunnelTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92CAAC529F7D33C008ED922 /* TunnelTransportProvider.swift */; }; + A97FF54B2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54A2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift */; }; E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; }; E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; }; E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; @@ -631,6 +638,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01F1FF1B29F06124007083C3 /* ShadowSocksProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowSocksProxy.swift; sourceTree = "<group>"; }; + 01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libshadowsocks_proxy.a; path = ../target/debug/libshadowsocks_proxy.a; sourceTree = "<group>"; }; 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDefaults.swift; sourceTree = "<group>"; }; 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyURLRequest.swift; sourceTree = "<group>"; }; 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelTransport.swift; sourceTree = "<group>"; }; @@ -777,6 +786,7 @@ 586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = "<group>"; }; 586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = "<group>"; }; 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = "<group>"; }; + 586F2BE129F6916F009E6924 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = shadowsocks.h; path = "shadowsocks-proxy/include/shadowsocks.h"; sourceTree = "<group>"; }; 5871167E2910035700D41AAC /* PreferencesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesInteractor.swift; sourceTree = "<group>"; }; 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; }; 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+IPAddress.swift"; sourceTree = "<group>"; }; @@ -971,6 +981,10 @@ 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = "<group>"; }; A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; }; A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; }; + A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; }; + A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; }; + A92CAAC529F7D33C008ED922 /* TunnelTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelTransportProvider.swift; sourceTree = "<group>"; }; + A97FF54A2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelTransportProvider.swift; sourceTree = "<group>"; }; E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; }; E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; }; E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; }; @@ -992,6 +1006,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 01F1FF1E29F0627D007083C3 /* libshadowsocks_proxy.a in Frameworks */, 58D223BF294C8AE90029F5F8 /* Operations.framework in Frameworks */, 58D2241D294C91D20029F5F8 /* MullvadLogging.framework in Frameworks */, 58D223DC294C8EB90029F5F8 /* MullvadTypes.framework in Frameworks */, @@ -1136,6 +1151,7 @@ 582FFA82290A84E700895745 /* Info.plist */, 062B45A228FD4C0F00746E77 /* Assets */, 06799ABE28F98E1D00ACD94E /* MullvadREST.h */, + 586F2BE129F6916F009E6924 /* shadowsocks.h */, 06AC114128F8413A0037AF9A /* AddressCache.swift */, 06FAE67128F83CA40033DD93 /* HTTP.swift */, 06FAE67B28F83CA50033DD93 /* REST.swift */, @@ -1159,10 +1175,12 @@ 5897F1732913EAF800AF5695 /* ExponentialBackoff.swift */, 06FAE67528F83CA40033DD93 /* RESTTaskIdentifier.swift */, 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */, + A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */, 06FAE66528F83CA30033DD93 /* RESTURLSession.swift */, 06FAE67728F83CA40033DD93 /* ServerRelaysResponse.swift */, 06FAE66B28F83CA30033DD93 /* SSLPinningURLSessionDelegate.swift */, 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */, + 01F1FF1B29F06124007083C3 /* ShadowSocksProxy.swift */, ); path = MullvadREST; sourceTree = "<group>"; @@ -1477,6 +1495,7 @@ children = ( 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */, 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */, + A97FF54A2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift */, ); path = SimulatorTunnelProvider; sourceTree = "<group>"; @@ -1546,6 +1565,7 @@ 584F991F2902CBDD001F858D /* Frameworks */ = { isa = PBXGroup; children = ( + 01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */, ); name = Frameworks; sourceTree = "<group>"; @@ -1825,6 +1845,7 @@ 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */, 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */, 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, + A92CAAC529F7D33C008ED922 /* TunnelTransportProvider.swift */, ); path = PacketTunnel; sourceTree = "<group>"; @@ -1940,6 +1961,7 @@ A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */, 58FBFBF0291630700020E046 /* DurationTests.swift */, 58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */, + A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */, ); path = MullvadRESTTests; sourceTree = "<group>"; @@ -1959,6 +1981,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 586F2BE229F6916F009E6924 /* shadowsocks.h in Headers */, 06799ACE28F98E1D00ACD94E /* MullvadREST.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2034,6 +2057,8 @@ isa = PBXNativeTarget; buildConfigurationList = 06799AD328F98E1D00ACD94E /* Build configuration list for PBXNativeTarget "MullvadREST" */; buildPhases = ( + 588E4EB028FEF1CA008046E3 /* Run prebuild script */, + 01F1FF1F29F07D63007083C3 /* ShellScript */, 06799AB728F98E1D00ACD94E /* Headers */, 06799AB828F98E1D00ACD94E /* Sources */, 06799AB928F98E1D00ACD94E /* Frameworks */, @@ -2477,6 +2502,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 01F1FF1F29F07D63007083C3 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nCARGO_TARGET_DIR=${PROJECT_DIR}/../target bash ${PROJECT_DIR}/MullvadREST/shadowsocks-proxy/build.sh shadowsocks-proxy\n"; + }; 063F028D2902BC8E001FA09F /* Run prebuild script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -2524,6 +2567,7 @@ 06799AEA28F98E4800ACD94E /* RESTProxy.swift in Sources */, 06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */, 06799ADB28F98E4800ACD94E /* RESTProxyFactory.swift in Sources */, + 01F1FF1C29F06124007083C3 /* ShadowSocksProxy.swift in Sources */, 06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */, 06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */, 06799AE628F98E4800ACD94E /* ServerRelaysResponse.swift in Sources */, @@ -2536,6 +2580,7 @@ 06799ADF28F98E4800ACD94E /* RESTDevicesProxy.swift in Sources */, 06799ADA28F98E4800ACD94E /* RESTResponseHandler.swift in Sources */, 062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */, + A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */, 06799AE428F98E4800ACD94E /* RESTAccountsProxy.swift in Sources */, 5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */, 06799AE328F98E4800ACD94E /* RESTNetworkOperation.swift in Sources */, @@ -2654,6 +2699,7 @@ 587C93002986E2B600FB9664 /* TermsOfServiceCoordinator.swift in Sources */, 5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */, 587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */, + A97FF54B2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift in Sources */, 5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */, 58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, @@ -2818,6 +2864,7 @@ 583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, 5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */, 58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */, + A92CAAC629F7D33C008ED922 /* TunnelTransportProvider.swift in Sources */, 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2906,6 +2953,7 @@ buildActionMask = 2147483647; files = ( A9CF11FD2A0518E7001D9565 /* AddressCacheTests.swift in Sources */, + A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */, 58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */, 58FBFBF1291630700020E046 /* DurationTests.swift in Sources */, ); @@ -3164,6 +3212,10 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug"; + MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/MullvadREST/module.private.modulemap; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadREST"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -3196,6 +3248,10 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release"; + MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/MullvadREST/module.private.modulemap; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadREST"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3b4fa835a2..0000000000 --- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", - "version" : "1.4.0" - } - }, - { - "identity" : "wireguard-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mullvad/wireguard-apple.git", - "state" : { - "revision" : "6baeac49a14313a7b8b7a956f67f4a47a6ae7a41" - } - } - ], - "version" : 2 -} diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 26ac51cccf..69af7faa45 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -59,9 +59,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) proxyFactory = REST.ProxyFactory.makeProxyFactory( - transportProvider: { [weak self] in - return self?.transportMonitor.transport - }, + transportProvider: { [weak self] in self?.transportMonitor }, addressCache: addressCache ) @@ -70,6 +68,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD devicesProxy = proxyFactory.createDevicesProxy() relayCacheTracker = RelayCacheTracker(application: application, apiProxy: apiProxy) + addressCacheTracker = AddressCacheTracker( application: application, apiProxy: apiProxy, @@ -93,7 +92,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy: accountsProxy ) - transportMonitor = TransportMonitor(tunnelManager: tunnelManager, tunnelStore: tunnelStore) + transportMonitor = TransportMonitor( + tunnelManager: tunnelManager, + tunnelStore: tunnelStore, + relayCacheTracker: relayCacheTracker + ) #if targetEnvironment(simulator) // Configure mock tunnel provider on simulator diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index 7d2331b92d..2078646be2 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -21,15 +21,22 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { private var selectorResult: RelaySelectorResult? private let urlRequestProxy: URLRequestProxy private let relayCacheTracker: RelayCacheTracker + private let simulatorTransportProvider: SimulatorTunnelTransportProvider private let providerLogger = Logger(label: "SimulatorTunnelProviderHost") private let dispatchQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue") init(relayCacheTracker: RelayCacheTracker) { self.relayCacheTracker = relayCacheTracker + + let urlSession = REST.makeURLSession() + let urlSessionTransport = REST.URLSessionTransport(urlSession: urlSession) + let simulatorTransportProvider = SimulatorTunnelTransportProvider(urlSessionTransport: urlSessionTransport) + self.simulatorTransportProvider = simulatorTransportProvider + self.urlRequestProxy = URLRequestProxy( - urlSession: REST.makeURLSession(), - dispatchQueue: dispatchQueue + dispatchQueue: dispatchQueue, + transportProvider: { simulatorTransportProvider } ) } diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelTransportProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelTransportProvider.swift new file mode 100644 index 0000000000..a571101e3f --- /dev/null +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelTransportProvider.swift @@ -0,0 +1,26 @@ +// +// SimulatorTunnelTransportProvider.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-05-10. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST + +final class SimulatorTunnelTransportProvider: RESTTransportProvider { + private let urlSessionTransport: REST.URLSessionTransport + + init(urlSessionTransport: REST.URLSessionTransport) { + self.urlSessionTransport = urlSessionTransport + } + + func transport() -> MullvadREST.RESTTransport? { + urlSessionTransport + } + + func shadowSocksTransport() -> MullvadREST.RESTTransport? { + urlSessionTransport + } +} diff --git a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift index 4ee628751d..3314fc3823 100644 --- a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift +++ b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift @@ -18,16 +18,28 @@ struct PacketTunnelTransport: RESTTransport { } let tunnel: Tunnel + let useShadowsocksTransport: Bool - init(tunnel: Tunnel) { + init(tunnel: Tunnel, useShadowsocksTransport: Bool) { self.tunnel = tunnel + self.useShadowsocksTransport = useShadowsocksTransport } func sendRequest( _ request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void - ) throws -> Cancellable { - let proxyRequest = try ProxyURLRequest(id: UUID(), urlRequest: request) + ) -> Cancellable { + let proxyRequest = ProxyURLRequest( + id: UUID(), + urlRequest: request, + useShadowsocksTransport: useShadowsocksTransport + ) + + // If the URL provided to the proxy request was invalid, indicate failure via `.badURL` and return a no-op. + guard let proxyRequest = proxyRequest else { + completion(nil, nil, URLError(.badURL)) + return AnyCancellable {} + } return tunnel.sendRequest(proxyRequest) { result in switch result { diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift index 9f02429348..e37c416682 100644 --- a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift +++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift @@ -7,21 +7,83 @@ // import Foundation +import MullvadLogging import MullvadREST +import RelayCache +import RelaySelector -final class TransportMonitor { +final class TransportMonitor: RESTTransportProvider { private let tunnelManager: TunnelManager private let tunnelStore: TunnelStore private let urlSessionTransport: REST.URLSessionTransport + private let relayCacheTracker: RelayCacheTracker + private let logger = Logger(label: "TransportMonitor") - init(tunnelManager: TunnelManager, tunnelStore: TunnelStore) { + // MARK: - + + // MARK: Public API + + init(tunnelManager: TunnelManager, tunnelStore: TunnelStore, relayCacheTracker: RelayCacheTracker) { self.tunnelManager = tunnelManager self.tunnelStore = tunnelStore + self.relayCacheTracker = relayCacheTracker urlSessionTransport = REST.URLSessionTransport(urlSession: REST.makeURLSession()) } - var transport: RESTTransport? { + public func transport() -> MullvadREST.RESTTransport? { + return selectTransport(urlSessionTransport, useShadowsocksTransport: false) + } + + public func shadowSocksTransport() -> RESTTransport? { + let shadowSocksTransport: RESTTransport + do { + let cachedRelays = try relayCacheTracker.getCachedRelays() + + let shadowSocksConfiguration = RelaySelector.getShadowsocksTCPBridge(relays: cachedRelays.relays) + let shadowSocksBridgeRelay = RelaySelector.getShadowSocksRelay(relays: cachedRelays.relays) + + guard let shadowSocksConfiguration = shadowSocksConfiguration, + let shadowSocksBridgeRelay = shadowSocksBridgeRelay + else { + logger.error("Could not get shadow socks bridge information.") + return nil + } + + let shadowSocksURLSession = urlSessionTransport.urlSession + let transport = REST.URLSessionShadowSocksTransport( + urlSession: shadowSocksURLSession, + shadowSocksConfiguration: shadowSocksConfiguration, + shadowSocksBridgeRelay: shadowSocksBridgeRelay + ) + + shadowSocksTransport = transport + } catch { + logger.error( + error: error, + message: "Could not create shadow socks transport." + ) + return nil + } + return selectTransport(shadowSocksTransport, useShadowsocksTransport: true) + } + + // MARK: - + + // MARK: Private API + + /// Selects a transport to use for sending an `URLRequest` + /// + /// This method returns the appropriate transport layer based on whether a tunnel is available, and whether it + /// should be bypassed + /// whenever a transport is requested. + /// + /// - Parameters: + /// - transport: The transport to use if there is no tunnel, or if it shouldn't be bypassed + /// - useShadowsocksTransport: A hint for enforcing a Shadowsocks transport when proxying a request via an + /// available `Tunnel` + /// - Returns: A transport to use for sending an `URLRequest` + private func selectTransport(_ transport: RESTTransport, useShadowsocksTransport: Bool) -> RESTTransport { let tunnel = tunnelStore.getPersistentTunnels().first { tunnel in return tunnel.status == .connecting || tunnel.status == .reasserting || @@ -29,10 +91,12 @@ final class TransportMonitor { } if let tunnel = tunnel, shouldByPassVPN(tunnel: tunnel) { - return PacketTunnelTransport(tunnel: tunnel) - } else { - return urlSessionTransport + return PacketTunnelTransport( + tunnel: tunnel, + useShadowsocksTransport: useShadowsocksTransport + ) } + return transport } private func shouldByPassVPN(tunnel: Tunnel) -> Bool { diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift index f0276f5ba0..f8f9eb60bc 100644 --- a/ios/MullvadVPNTests/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/RelaySelectorTests.swift @@ -152,5 +152,5 @@ private let sampleRelays = REST.ServerRelaysResponse( ), ] ), - bridge: REST.ServerBridges(shadowsocks: []) + bridge: REST.ServerBridges(shadowsocks: [], relays: []) ) diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 4a0a341aba..fcae0e4ec4 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -152,14 +152,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { let urlSession = REST.makeURLSession() let urlSessionTransport = REST.URLSessionTransport(urlSession: urlSession) + let transportProvider = TunnelTransportProvider( + urlSessionTransport: urlSessionTransport, + relayCache: relayCache + ) + let proxyFactory = REST.ProxyFactory.makeProxyFactory( - transportProvider: { () -> RESTTransport? in - return urlSessionTransport - }, + transportProvider: { transportProvider }, addressCache: addressCache ) - urlRequestProxy = URLRequestProxy(urlSession: urlSession, dispatchQueue: dispatchQueue) + urlRequestProxy = URLRequestProxy( + dispatchQueue: dispatchQueue, + transportProvider: { transportProvider } + ) accountsProxy = proxyFactory.createAccountsProxy() devicesProxy = proxyFactory.createDevicesProxy() diff --git a/ios/PacketTunnel/TunnelTransportProvider.swift b/ios/PacketTunnel/TunnelTransportProvider.swift new file mode 100644 index 0000000000..85e0600c49 --- /dev/null +++ b/ios/PacketTunnel/TunnelTransportProvider.swift @@ -0,0 +1,55 @@ +// +// TunnelTransportProvider.swift +// PacketTunnel +// +// Created by Marco Nikic on 2023-04-25. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging +import MullvadREST +import RelayCache +import RelaySelector + +final class TunnelTransportProvider: RESTTransportProvider { + private let urlSessionTransport: REST.URLSessionTransport + private let relayCache: RelayCache + private let logger = Logger(label: "TunnelTransportProvider") + + init(urlSessionTransport: REST.URLSessionTransport, relayCache: RelayCache) { + self.urlSessionTransport = urlSessionTransport + self.relayCache = relayCache + } + + func transport() -> MullvadREST.RESTTransport? { + urlSessionTransport + } + + func shadowSocksTransport() -> MullvadREST.RESTTransport? { + do { + let cachedRelays = try relayCache.read() + let shadowSocksConfiguration = RelaySelector.getShadowsocksTCPBridge(relays: cachedRelays.relays) + let shadowSocksBridgeRelay = RelaySelector.getShadowSocksRelay(relays: cachedRelays.relays) + + guard let shadowSocksConfiguration = shadowSocksConfiguration, + let shadowSocksBridgeRelay = shadowSocksBridgeRelay + else { + logger.error("Could not get shadow socks bridge information.") + return nil + } + + let shadowSocksURLSession = urlSessionTransport.urlSession + let shadowSocksTransport = REST.URLSessionShadowSocksTransport( + urlSession: shadowSocksURLSession, + shadowSocksConfiguration: shadowSocksConfiguration, + shadowSocksBridgeRelay: shadowSocksBridgeRelay + ) + + return shadowSocksTransport + } catch { + logger.error(error: error) + } + return nil + } +} diff --git a/ios/RelaySelector/RelaySelector.swift b/ios/RelaySelector/RelaySelector.swift index 0feed32cf4..11625b1ca1 100644 --- a/ios/RelaySelector/RelaySelector.swift +++ b/ios/RelaySelector/RelaySelector.swift @@ -20,6 +20,13 @@ public enum RelaySelector { return relays.bridge.shadowsocks.filter { $0.protocol == "tcp" }.randomElement() } + /// Return a random Shadowsocks bridge relay, or `nil` if no relay were found. + /// - Parameter relays: The list of relays to randomly select from + /// - Returns: A Shadowsocks relay or `nil` if no relay were found. + public static func getShadowSocksRelay(relays: REST.ServerRelaysResponse) -> REST.BridgeRelay? { + relays.bridge.relays.randomElement() + } + /** Filters relay list using given constraints and selects random relay. Throws an error if there are no relays satisfying the given constraints. diff --git a/ios/TunnelProviderMessaging/ProxyURLRequest.swift b/ios/TunnelProviderMessaging/ProxyURLRequest.swift index acc346f2fc..cf392eee12 100644 --- a/ios/TunnelProviderMessaging/ProxyURLRequest.swift +++ b/ios/TunnelProviderMessaging/ProxyURLRequest.swift @@ -15,6 +15,7 @@ public struct ProxyURLRequest: Codable { public let method: String? public let httpBody: Data? public let httpHeaders: [String: String]? + public let useShadowsocksTransport: Bool public var urlRequest: URLRequest { var urlRequest = URLRequest(url: url) @@ -24,21 +25,14 @@ public struct ProxyURLRequest: Codable { return urlRequest } - public init(id: UUID, urlRequest: URLRequest) throws { - guard let url = urlRequest.url else { - throw InvalidURLRequestError() - } + public init?(id: UUID, urlRequest: URLRequest, useShadowsocksTransport: Bool = false) { + guard let urlRequestUrl = urlRequest.url else { return nil } self.id = id - self.url = url + url = urlRequestUrl method = urlRequest.httpMethod httpBody = urlRequest.httpBody httpHeaders = urlRequest.allHTTPHeaderFields - } -} - -public struct InvalidURLRequestError: LocalizedError { - public var errorDescription: String? { - return "Invalid URLRequest URL." + self.useShadowsocksTransport = useShadowsocksTransport } } diff --git a/ios/TunnelProviderMessaging/URLRequestProxy.swift b/ios/TunnelProviderMessaging/URLRequestProxy.swift index 8d66819ebb..931cb4a571 100644 --- a/ios/TunnelProviderMessaging/URLRequestProxy.swift +++ b/ios/TunnelProviderMessaging/URLRequestProxy.swift @@ -7,20 +7,24 @@ // import Foundation +import MullvadREST +import MullvadTypes public final class URLRequestProxy { /// Serial queue used for synchronizing access to class members. private let dispatchQueue: DispatchQueue - /// URL session used for proxy requests. - private let urlSession: URLSession + private let transportProvider: () -> RESTTransportProvider? /// List of all proxied network requests bypassing VPN. - private var proxiedRequests: [UUID: URLSessionDataTask] = [:] + private var proxiedRequests: [UUID: Cancellable] = [:] - public init(urlSession: URLSession, dispatchQueue: DispatchQueue) { - self.urlSession = urlSession + public init( + dispatchQueue: DispatchQueue, + transportProvider: @escaping () -> RESTTransportProvider? + ) { self.dispatchQueue = dispatchQueue + self.transportProvider = transportProvider } public func sendRequest( @@ -28,29 +32,28 @@ public final class URLRequestProxy { completionHandler: @escaping (ProxyURLResponse) -> Void ) { dispatchQueue.async { - let task = self.urlSession - .dataTask(with: proxyRequest.urlRequest) { [weak self] data, response, error in - guard let self = self else { return } - - self.dispatchQueue.async { - let response = ProxyURLResponse( - data: data, - response: response, - error: error - ) - - _ = self.removeRequest(identifier: proxyRequest.id) + // Instruct the Packet Tunnel to try to reach the API via the local shadow socks proxy instance if needed + let transportProvider = self.transportProvider() + let transport = proxyRequest.useShadowsocksTransport + ? transportProvider?.shadowSocksTransport() + : transportProvider?.transport() + guard let transport = transport else { return } + // The task sent by `transport.sendRequest` comes in an already resumed state + let task = transport.sendRequest(proxyRequest.urlRequest) { [weak self] data, response, error in + guard let self = self else { return } + // However there is no guarantee about which queue the execution resumes on + // Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests` + self.dispatchQueue.async { + let response = ProxyURLResponse(data: data, response: response, error: error) + _ = self.removeRequest(identifier: proxyRequest.id) - completionHandler(response) - } + completionHandler(response) } - + } // All tasks should have unique identifiers, but if not, cancel the task scheduled // earlier. let oldTask = self.addRequest(identifier: proxyRequest.id, task: task) oldTask?.cancel() - - task.resume() } } @@ -62,11 +65,13 @@ public final class URLRequestProxy { } } - private func addRequest(identifier: UUID, task: URLSessionDataTask) -> URLSessionDataTask? { + private func addRequest(identifier: UUID, task: Cancellable) -> Cancellable? { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) return proxiedRequests.updateValue(task, forKey: identifier) } - private func removeRequest(identifier: UUID) -> URLSessionDataTask? { + private func removeRequest(identifier: UUID) -> Cancellable? { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) return proxiedRequests.removeValue(forKey: identifier) } } |
