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 | |
| parent | 77f51e690b26346ff4b251c27eb2ece493820d85 (diff) | |
| download | mullvadvpn-e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2.tar.xz mullvadvpn-e5b73d7abc1b50cfbb9c6f056bf66fef28fef5b2.zip | |
Add shadowsocks-proxy crate
40 files changed, 1251 insertions, 170 deletions
diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index af2d2c70de..b58870edb4 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -51,6 +51,8 @@ jobs: uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '14.3' + - name: Configure Rust + run: rustup target add x86_64-apple-ios - name: Configure Xcode project run: | diff --git a/Cargo.lock b/Cargo.lock index 3a1659baf2..5bad5a1c61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,6 +380,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" dependencies = [ + "clap 3.2.25", "heck", "indexmap", "log", @@ -463,6 +464,21 @@ dependencies = [ [[package]] name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags", + "clap_lex 0.2.4", + "indexmap", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap" version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" @@ -481,7 +497,7 @@ dependencies = [ "anstream", "anstyle", "bitflags", - "clap_lex", + "clap_lex 0.4.1", "once_cell", "strsim 0.10.0", ] @@ -492,7 +508,7 @@ version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a19591b2ab0e3c04b588a0e04ddde7b9eaa423646d1b4a8092879216bf47473" dependencies = [ - "clap", + "clap 4.2.7", ] [[package]] @@ -509,6 +525,15 @@ dependencies = [ [[package]] name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" @@ -595,6 +620,25 @@ dependencies = [ ] [[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crypto-bigint" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -685,6 +729,17 @@ dependencies = [ ] [[package]] +name = "dashmap" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8858831f7781322e539ea39e72449c46b059638250c14344fec8d0aa6e539c" +dependencies = [ + "cfg-if", + "num_cpus", + "parking_lot", +] + +[[package]] name = "data-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -994,6 +1049,18 @@ dependencies = [ ] [[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.48.0", +] + +[[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1007,15 +1074,23 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] name = "futures" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1203,6 +1278,15 @@ dependencies = [ [[package]] name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" @@ -1285,9 +1369,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.5.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -1303,9 +1387,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", @@ -1316,7 +1400,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -1389,6 +1473,17 @@ dependencies = [ [[package]] name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" @@ -1589,6 +1684,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" [[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1596,9 +1711,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libdbus-sys" @@ -1816,7 +1931,7 @@ dependencies = [ "anyhow", "base64 0.13.0", "chrono", - "clap", + "clap 4.2.7", "clap_complete", "env_logger 0.10.0", "futures", @@ -1840,7 +1955,7 @@ dependencies = [ "android_logger", "cfg-if", "chrono", - "clap", + "clap 4.2.7", "ctrlc", "dirs-next", "duct", @@ -1969,7 +2084,7 @@ dependencies = [ name = "mullvad-problem-report" version = "0.0.0" dependencies = [ - "clap", + "clap 4.2.7", "dirs-next", "duct", "env_logger 0.10.0", @@ -2014,7 +2129,7 @@ dependencies = [ name = "mullvad-setup" version = "0.0.0" dependencies = [ - "clap", + "clap 4.2.7", "env_logger 0.10.0", "err-derive", "lazy_static", @@ -2034,7 +2149,7 @@ name = "mullvad-types" version = "0.0.0" dependencies = [ "chrono", - "clap", + "clap 4.2.7", "err-derive", "ipnetwork", "jnix", @@ -2202,6 +2317,24 @@ dependencies = [ ] [[package]] +name = "notify" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify 0.9.6", + "kqueue", + "libc", + "mio", + "walkdir", + "windows-sys 0.42.0", +] + +[[package]] name = "num-integer" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2222,11 +2355,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.2.6", "libc", ] @@ -2288,6 +2421,23 @@ dependencies = [ ] [[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + +[[package]] name = "p256" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2352,9 +2502,9 @@ checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" @@ -3092,9 +3242,9 @@ dependencies = [ [[package]] name = "shadowsocks" version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4137cd7a208461a72d1901ea4990aa8a7307bce070bfe93a01c1cb827c4104a4" +source = "git+https://github.com/mullvad/shadowsocks-rust?rev=8f6afd081a9440fff2dda565908eddc27a3295f1#8f6afd081a9440fff2dda565908eddc27a3295f1" dependencies = [ + "arc-swap", "async-trait", "base64 0.21.0", "blake3", @@ -3104,6 +3254,7 @@ dependencies = [ "futures", "libc", "log", + "notify", "once_cell", "percent-encoding", "pin-project", @@ -3117,6 +3268,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tfo", + "trust-dns-resolver", "url", "windows-sys 0.45.0", ] @@ -3141,10 +3293,21 @@ dependencies = [ ] [[package]] +name = "shadowsocks-proxy" +version = "0.0.0" +dependencies = [ + "cbindgen", + "libc", + "log", + "oslog", + "shadowsocks-service", + "tokio", +] + +[[package]] name = "shadowsocks-service" version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22afe1a575e51616709fa0cdae9101d6eea7af95941a27a032801e24bd7b730a" +source = "git+https://github.com/mullvad/shadowsocks-rust?rev=8f6afd081a9440fff2dda565908eddc27a3295f1#8f6afd081a9440fff2dda565908eddc27a3295f1" dependencies = [ "arc-swap", "async-trait", @@ -3153,6 +3316,7 @@ dependencies = [ "bytes", "cfg-if", "futures", + "hyper", "idna 0.3.0", "ipnet", "iprange", @@ -3171,6 +3335,7 @@ dependencies = [ "spin 0.9.2", "thiserror", "tokio", + "tower", "windows-sys 0.45.0", ] @@ -3238,9 +3403,9 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -3378,7 +3543,7 @@ dependencies = [ "err-derive", "futures", "hex", - "inotify", + "inotify 0.10.0", "internet-checksum", "ipnetwork", "jnix", @@ -3403,7 +3568,6 @@ dependencies = [ "regex", "resolv-conf", "rtnetlink", - "shadowsocks-service", "shell-escape", "socket2", "subslice", @@ -3667,6 +3831,12 @@ dependencies = [ ] [[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] name = "thiserror" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4168,13 +4338,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna 0.2.3", - "matches", + "idna 0.3.0", "percent-encoding", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 3e7a3bed2f..a496374515 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "android/translations-converter", + "ios/MullvadREST/shadowsocks-proxy", "mullvad-daemon", "mullvad-cli", "mullvad-fs", 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) } } diff --git a/mullvad-api/Cargo.toml b/mullvad-api/Cargo.toml index e31d54c5e3..fb40f49022 100644 --- a/mullvad-api/Cargo.toml +++ b/mullvad-api/Cargo.toml @@ -32,4 +32,4 @@ mullvad-types = { path = "../mullvad-types" } talpid-types = { path = "../talpid-types" } talpid-time = { path = "../talpid-time" } -shadowsocks = { version = "1.15.3", default-features = false, features = ["stream-cipher"] } +shadowsocks = { git = "https://github.com/mullvad/shadowsocks-rust", rev = "8f6afd081a9440fff2dda565908eddc27a3295f1", features = [ "stream-cipher" ] } diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index 73007f32f9..54dc98c9cd 100644 --- a/mullvad-management-interface/src/types/conversions/settings.rs +++ b/mullvad-management-interface/src/types/conversions/settings.rs @@ -161,7 +161,8 @@ impl TryFrom<proto::Settings> for mullvad_types::settings::Settings { )?, // NOTE: This field is meaningless when obtained from gRPC wg_migration_rand_num: std::f32::NAN, - // NOTE: This field is set based on mullvad-types. It's not based on the actual settings version. + // NOTE: This field is set based on mullvad-types. It's not based on the actual settings + // version. settings_version: CURRENT_SETTINGS_VERSION, }) } diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 3d38b35578..9a833353d3 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -37,7 +37,6 @@ rand = "0.8.5" [target.'cfg(not(target_os="android"))'.dependencies] byteorder = "1" internet-checksum = "0.2" -shadowsocks-service = { version = "1.15.3", default-features = false, features = ["local", "stream-cipher"] } shell-escape = "0.1" socket2 = { version = "0.4.2", features = ["all"] } prost = "0.11" diff --git a/talpid-openvpn/Cargo.toml b/talpid-openvpn/Cargo.toml index e4fb576692..16fa1c7e8a 100644 --- a/talpid-openvpn/Cargo.toml +++ b/talpid-openvpn/Cargo.toml @@ -26,7 +26,7 @@ talpid-tunnel = { path = "../talpid-tunnel" } talpid-types = { path = "../talpid-types" } uuid = { version = "0.8", features = ["v4"] } tokio = { version = "1.8", features = ["process", "rt-multi-thread", "fs"] } -shadowsocks-service = { version = "1.15.3", default-features = false, features = ["local", "stream-cipher"] } +shadowsocks-service = { git = "https://github.com/mullvad/shadowsocks-rust", rev = "8f6afd081a9440fff2dda565908eddc27a3295f1", features = [ "local", "stream-cipher" ] } [target.'cfg(not(target_os="android"))'.dependencies] byteorder = "1" |
