summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Transport/TransportProvider.swift
blob: c9e37c575fdbe47d3693e977c9f3e5bf2ef26136 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//
//  TransportProvider.swift
//  MullvadTransport
//
//  Created by Marco Nikic on 2023-05-25.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import Logging
import MullvadRustRuntime
import MullvadTypes

public final class TransportProvider: RESTTransportProvider, Sendable {
    private let urlSessionTransport: URLSessionTransport
    private let addressCache: REST.AddressCache
    nonisolated(unsafe) private var transportStrategy: TransportStrategy
    nonisolated(unsafe) private var currentTransport: RESTTransport?
    nonisolated(unsafe) private var currentTransportType: TransportStrategy.Transport
    private let parallelRequestsMutex = NSLock()
    private let encryptedDNSTransport: RESTTransport

    public init(
        urlSessionTransport: URLSessionTransport,
        addressCache: REST.AddressCache,
        transportStrategy: TransportStrategy,
        encryptedDNSTransport: RESTTransport
    ) {
        self.urlSessionTransport = urlSessionTransport
        self.addressCache = addressCache
        self.transportStrategy = transportStrategy
        self.currentTransportType = transportStrategy.connectionTransport()
        self.encryptedDNSTransport = encryptedDNSTransport
    }

    public func makeTransport() -> RESTTransport? {
        parallelRequestsMutex.withLock {
            guard let actualTransport = makeTransportInner() else { return nil }

            let currentStrategy = transportStrategy
            return TransportWrapper(wrapped: actualTransport) { [weak self] error in
                if (error as? URLError)?.shouldResetNetworkTransport ?? false
                    || (error as? EncryptedDnsProxyError)?.shouldResetNetworkTransport ?? false
                {
                    self?.resetTransportMatching(currentStrategy)
                }
            }
        }
    }

    /// When several requests fail at the same time,  prevents the `transportStrategy` from switching multiple times.
    ///
    /// The `strategy` is checked against the `transportStrategy`. When several requests are made and fail in parallel,
    /// only the first failure will pass the equality check.
    /// Subsequent failures will not cause the strategy to change several times in a quick fashion.
    /// - Parameter strategy: The strategy object used when sending a request
    private func resetTransportMatching(_ strategy: TransportStrategy) {
        parallelRequestsMutex.lock()
        defer { parallelRequestsMutex.unlock() }

        if strategy == transportStrategy {
            if strategy.connectionTransport() == .encryptedDNS {
                (encryptedDNSTransport as? EncryptedDNSTransport)?.stop()
            }
            transportStrategy.didFail()
            currentTransport = nil
        }
    }

    /// Sets and returns the `currentTransport` according to the suggestion from `transportStrategy`
    ///
    /// > Warning: Do not  lock the `parallelRequestsMutex` in this method
    ///
    /// - Returns: A `RESTTransport` object to make a connection
    private func makeTransportInner() -> RESTTransport? {
        if currentTransport == nil || shouldNotReuseCurrentTransport {
            currentTransportType = transportStrategy.connectionTransport()
            switch currentTransportType {
            case .direct:
                currentTransport = urlSessionTransport
            case let .shadowsocks(configuration):
                currentTransport = ShadowsocksTransport(
                    urlSession: urlSessionTransport.urlSession,
                    configuration: configuration,
                    addressCache: addressCache
                )
            case let .socks5(configuration):
                currentTransport = URLSessionSocks5Transport(
                    urlSession: urlSessionTransport.urlSession,
                    configuration: configuration,
                    addressCache: addressCache
                )
            case .encryptedDNS:
                currentTransport = encryptedDNSTransport
            case .none:
                currentTransport = nil
            }
        }
        return currentTransport
    }

    /// The `Main` allows modifications to access methods through the UI.
    /// The `TransportProvider` relies on a `CurrentTransport` value set during build time or network error.
    /// To ensure  both process `Packet Tunnel` and `Main` uses the latest changes, the `TransportProvider` compares the `transportType` with the latest value in the cache and reuse it if it's still valid .
    private var shouldNotReuseCurrentTransport: Bool {
        currentTransportType != transportStrategy.connectionTransport()
    }
}

private extension EncryptedDnsProxyError {
    var shouldResetNetworkTransport: Bool {
        switch self {
        case .start:
            return true
        }
    }
}

private extension URLError {
    /// Whether the transport selection should be reset.
    ///
    /// `true` if the network request
    ///  * Was not cancelled
    ///  * Was not done during a phone call
    ///  * Was made when internet connection was available
    ///  * Was made in a context with data roaming, but international roaming was turned off
    var shouldResetNetworkTransport: Bool {
        code != .cancelled && code != .notConnectedToInternet && code != .internationalRoamingOff
            && code != .callIsActive
    }
}

/// Interstitial implementation of `RESTTransport` that intercepts the completion of the wrapped transport.
private struct TransportWrapper: RESTTransport {
    let wrapped: RESTTransport
    let onComplete: @Sendable (Error?) -> Void

    var name: String {
        return wrapped.name
    }

    func sendRequest(
        _ request: URLRequest,
        completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
    ) -> Cancellable {
        return wrapped.sendRequest(request) { data, response, error in
            onComplete(error)
            completion(data, response, error)
        }
    }
}