summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2023-04-19 11:08:52 +0100
committerEmīls <emils@mullvad.net>2023-05-22 10:18:27 +0200
commite5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2 (patch)
tree4e0c6cf3336c69c1c97b0640d5c06bc27819594c /ios
parent77f51e690b26346ff4b251c27eb2ece493820d85 (diff)
downloadmullvadvpn-e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2.tar.xz
mullvadvpn-e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2.zip
Add shadowsocks-proxy crate
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadREST/RESTNetworkOperation.swift56
-rw-r--r--ios/MullvadREST/RESTProxy.swift4
-rw-r--r--ios/MullvadREST/RESTProxyFactory.swift2
-rw-r--r--ios/MullvadREST/RESTTransport.swift12
-rw-r--r--ios/MullvadREST/RESTTransportStrategy.swift47
-rw-r--r--ios/MullvadREST/RESTURLSession.swift23
-rw-r--r--ios/MullvadREST/ServerRelaysResponse.swift15
-rw-r--r--ios/MullvadREST/ShadowSocksProxy.swift83
-rw-r--r--ios/MullvadREST/URLSessionTransport.swift46
-rw-r--r--ios/MullvadREST/module.private.modulemap5
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/.gitignore1
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/Cargo.toml25
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/build.rs14
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/build.sh57
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/include/shadowsocks.h20
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/src/bin/run.rs17
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/src/bin/run_unsafe.rs51
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/src/ffi.rs128
-rw-r--r--ios/MullvadREST/shadowsocks-proxy/src/lib.rs160
-rw-r--r--ios/MullvadRESTTests/TransportStrategyTests.swift40
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj56
-rw-r--r--ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved22
-rw-r--r--ios/MullvadVPN/AppDelegate.swift11
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift11
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelTransportProvider.swift26
-rw-r--r--ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift18
-rw-r--r--ios/MullvadVPN/TransportMonitor/TransportMonitor.swift76
-rw-r--r--ios/MullvadVPNTests/RelaySelectorTests.swift2
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift14
-rw-r--r--ios/PacketTunnel/TunnelTransportProvider.swift55
-rw-r--r--ios/RelaySelector/RelaySelector.swift7
-rw-r--r--ios/TunnelProviderMessaging/ProxyURLRequest.swift16
-rw-r--r--ios/TunnelProviderMessaging/URLRequestProxy.swift53
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)
}
}