summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2023-12-12 13:31:35 +0100
committerBug Magnet <marco.nikic@mullvad.net>2023-12-12 13:31:35 +0100
commitce78024b7f0db0d60062b141c6c4f722d218d9fd (patch)
tree8cfba8a53aead5c30bdfe6893e17e36389f15e91
parent821d36f77be449085eb8d4ebf4a9ea3db27259d3 (diff)
parentacc777d02bb0748fe94391b9ec4eea530c382731 (diff)
downloadmullvadvpn-ce78024b7f0db0d60062b141c6c4f722d218d9fd.tar.xz
mullvadvpn-ce78024b7f0db0d60062b141c6c4f722d218d9fd.zip
Merge branch 'socks5-proxy'
-rw-r--r--ios/MullvadREST/Transport/Direct/URLSessionTransport.swift2
-rw-r--r--ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift33
-rw-r--r--ios/MullvadREST/Transport/Socks5/CancellableChain.swift43
-rw-r--r--ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift22
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift15
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift14
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Command.swift15
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift21
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5ConnectCommand.swift46
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift109
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Connection.swift227
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Constants.swift13
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift80
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift111
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift124
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Error.swift54
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift281
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Handshake.swift43
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift68
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift21
-rw-r--r--ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift112
-rw-r--r--ios/MullvadREST/Transport/TransportProvider.swift13
-rw-r--r--ios/MullvadREST/Transport/TransportStrategy.swift2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj88
24 files changed, 1556 insertions, 1 deletions
diff --git a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift
index fc5aee683b..77c5b48e59 100644
--- a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift
+++ b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift
@@ -1,6 +1,6 @@
//
// URLSessionTransport.swift
-// MullvadREST
+// MullvadTransport
//
// Created by Mojgan on 2023-12-08.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
diff --git a/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift b/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift
new file mode 100644
index 0000000000..986f8276fa
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift
@@ -0,0 +1,33 @@
+//
+// AnyIPEndpoint+Socks5.swift
+// MullvadTransport
+//
+// Created by pronebird on 23/10/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import Network
+
+extension AnyIPEndpoint {
+ /// Convert `AnyIPEndpoint` to `Socks5Endpoint`.
+ var socksEndpoint: Socks5Endpoint {
+ switch self {
+ case let .ipv4(endpoint):
+ .ipv4(endpoint)
+ case let .ipv6(endpoint):
+ .ipv6(endpoint)
+ }
+ }
+
+ /// Convert `AnyIPEndpoint` to `NWEndpoint`.
+ var nwEndpoint: NWEndpoint {
+ switch self {
+ case let .ipv4(endpoint):
+ .hostPort(host: .ipv4(endpoint.ip), port: NWEndpoint.Port(integerLiteral: endpoint.port))
+ case let .ipv6(endpoint):
+ .hostPort(host: .ipv6(endpoint.ip), port: NWEndpoint.Port(integerLiteral: endpoint.port))
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/CancellableChain.swift b/ios/MullvadREST/Transport/Socks5/CancellableChain.swift
new file mode 100644
index 0000000000..510d76094a
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/CancellableChain.swift
@@ -0,0 +1,43 @@
+//
+// CancellableChain.swift
+// MullvadTransport
+//
+// Created by pronebird on 23/10/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+/// Cancellable object that cancels all cancellable objects linked to it.
+final class CancellableChain: Cancellable {
+ private let stateLock = NSLock()
+ private var isCancelled = false
+ private var linkedTokens: [Cancellable] = []
+
+ init() {}
+
+ /// Link cancellation token with some other.
+ ///
+ /// The token is cancelled immediately, if the chain is already cancelled.
+ func link(_ token: Cancellable) {
+ stateLock.withLock {
+ if isCancelled {
+ token.cancel()
+ } else {
+ linkedTokens.append(token)
+ }
+ }
+ }
+
+ /// Request cancellation.
+ ///
+ /// Cancels and releases any of the connected tokens.
+ func cancel() {
+ stateLock.withLock {
+ isCancelled = true
+ linkedTokens.forEach { $0.cancel() }
+ linkedTokens.removeAll()
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift b/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift
new file mode 100644
index 0000000000..d99f9e8081
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift
@@ -0,0 +1,22 @@
+//
+// NWConnection+Extensions.swift
+// MullvadTransport
+//
+// Created by pronebird on 20/10/2023.
+//
+
+import Foundation
+import Network
+
+extension NWConnection {
+ /**
+ Read exact number of bytes from connection.
+
+ - Parameters:
+ - exactLength: exact number of bytes to read.
+ - completion: a completion handler.
+ */
+ func receive(exactLength: Int, completion: @escaping (Data?, ContentContext?, Bool, NWError?) -> Void) {
+ receive(minimumIncompleteLength: exactLength, maximumLength: exactLength, completion: completion)
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift b/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift
new file mode 100644
index 0000000000..ad013d1ae9
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift
@@ -0,0 +1,15 @@
+//
+// Socks5AddressType.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+
+/// Address type supported by socks protocol
+enum Socks5AddressType: UInt8 {
+ case ipv4 = 0x01
+ case domainName = 0x03
+ case ipv6 = 0x04
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift
new file mode 100644
index 0000000000..39a240d41e
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift
@@ -0,0 +1,14 @@
+//
+// Socks5Authentication.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+
+/// Authentication methods supported by socks protocol.
+enum Socks5AuthenticationMethod: UInt8 {
+ case notRequired = 0x00
+ case usernamePassword = 0x02
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Command.swift b/ios/MullvadREST/Transport/Socks5/Socks5Command.swift
new file mode 100644
index 0000000000..293acf218b
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Command.swift
@@ -0,0 +1,15 @@
+//
+// Socks5Command.swift
+// MullvadTransport
+//
+// Created by pronebird on 21/10/2023.
+//
+
+import Foundation
+
+/// Commands supported in socks protocol.
+enum Socks5Command: UInt8 {
+ case connect = 0x01
+ case bind = 0x02
+ case udpAssociate = 0x03
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift
new file mode 100644
index 0000000000..39033821d4
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift
@@ -0,0 +1,21 @@
+//
+// Socks5Configuration.swift
+// MullvadTransport
+//
+// Created by pronebird on 23/10/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+/// Socks5 configuration.
+/// - See: ``URLSessionSocks5Transport``
+public struct Socks5Configuration {
+ /// The socks proxy endpoint.
+ public var proxyEndpoint: AnyIPEndpoint
+
+ public init(proxyEndpoint: AnyIPEndpoint) {
+ self.proxyEndpoint = proxyEndpoint
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ConnectCommand.swift b/ios/MullvadREST/Transport/Socks5/Socks5ConnectCommand.swift
new file mode 100644
index 0000000000..0ad4d23bbd
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5ConnectCommand.swift
@@ -0,0 +1,46 @@
+//
+// Socks5ConnectCommand.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+import Network
+
+/// The connect command message.
+struct Socks5ConnectCommand {
+ /// The remote endpoint to which the client wants to establish connection over the socks proxy.
+ var endpoint: Socks5Endpoint
+
+ /// The byte representation in socks protocol.
+ var rawData: Data {
+ var data = Data()
+
+ // Socks version.
+ data.append(Socks5Constants.socksVersion)
+
+ // Command code.
+ data.append(Socks5Command.connect.rawValue)
+
+ // Reserved.
+ data.append(0)
+
+ // Address type.
+ data.append(endpoint.addressType.rawValue)
+
+ // Endpoint address.
+ data.append(endpoint.rawData)
+
+ return data
+ }
+}
+
+/// The connect command reply message.
+struct Socks5ConnectReply {
+ /// The server status code.
+ var status: Socks5StatusCode
+
+ /// The server bound endpoint.
+ var serverBoundEndpoint: Socks5Endpoint
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift b/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift
new file mode 100644
index 0000000000..cbd4f0875e
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift
@@ -0,0 +1,109 @@
+//
+// Socks5ConnectNegotiation.swift
+// MullvadTransport
+//
+// Created by pronebird on 20/10/2023.
+//
+
+import Foundation
+import Network
+
+/// The object handling a connection negotiation with socks proxy.
+struct Socks5ConnectNegotiation {
+ /// Connection to the socks proxy.
+ let connection: NWConnection
+
+ /// Endpoint to which the client wants to initiate connection over socks proxy.
+ let endpoint: Socks5Endpoint
+
+ /// Completion handler invoked on success.
+ let onComplete: (Socks5ConnectReply) -> Void
+
+ /// Failure handler invoked on error.
+ let onFailure: (Error) -> Void
+
+ /// Initiate negotiation by sending a connect command to the socks proxy.
+ func perform() {
+ let connectCommand = Socks5ConnectCommand(endpoint: endpoint)
+
+ connection.send(content: connectCommand.rawData, completion: .contentProcessed { [self] error in
+ if let error {
+ onFailure(Socks5Error.remoteConnectionFailure(error))
+ } else {
+ readPartialReply()
+ }
+ })
+ }
+
+ /// Read the preamble of the connect reply.
+ private func readPartialReply() {
+ // The length of the preamble of the CONNECT reply.
+ let replyPreambleLength = 4
+
+ connection.receive(exactLength: replyPreambleLength) { [self] data, _, _, error in
+ if let error {
+ onFailure(Socks5Error.remoteConnectionFailure(error))
+ } else if let data {
+ do {
+ try handlePartialReply(data: data)
+ } catch {
+ onFailure(error)
+ }
+ } else {
+ onFailure(Socks5Error.unexpectedEndOfStream)
+ }
+ }
+ }
+
+ /**
+ Parse the bytes that comprise the preamble of a connect reply. Upon success read the endpoint data to produce the complete reply and finish negotiation.
+
+ The following fields are contained within the first 4 bytes: socks version, status code, reserved field, address type.
+ */
+ private func handlePartialReply(data: Data) throws {
+ // Parse partial reply that contains the status code and address type.
+ let (statusCode, addressType) = try parsePartialReply(data: data)
+
+ // Parse server bound endpoint to produce the complete reply.
+ let endpointReader = Socks5EndpointReader(
+ connection: connection,
+ addressType: addressType,
+ onComplete: { [self] endpoint in
+ let reply = Socks5ConnectReply(status: statusCode, serverBoundEndpoint: endpoint)
+ onComplete(reply)
+ },
+ onFailure: onFailure
+ )
+ endpointReader.perform()
+ }
+
+ /// Parse the bytes that comprise the preamble of reply without endpoint data.
+ private func parsePartialReply(data: Data) throws -> (Socks5StatusCode, Socks5AddressType) {
+ var iterator = data.makeIterator()
+
+ // Read the protocol version.
+ guard let version = iterator.next() else { throw Socks5Error.unexpectedEndOfStream }
+
+ // Verify the protocol version.
+ guard version == Socks5Constants.socksVersion else { throw Socks5Error.invalidSocksVersion }
+
+ // Read status code, reserved field and address type from reply.
+ guard let rawStatusCode = iterator.next(),
+ iterator.next() != nil, // skip reserved field
+ let rawAddressType = iterator.next() else {
+ throw Socks5Error.unexpectedEndOfStream
+ }
+
+ // Parse the status code.
+ guard let status = Socks5StatusCode(rawValue: rawStatusCode) else {
+ throw Socks5Error.invalidStatusCode(rawStatusCode)
+ }
+
+ // Parse the address type.
+ guard let addressType = Socks5AddressType(rawValue: rawAddressType) else {
+ throw Socks5Error.invalidAddressType
+ }
+
+ return (status, addressType)
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift
new file mode 100644
index 0000000000..a3163029fe
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift
@@ -0,0 +1,227 @@
+//
+// Socks5Connection.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+import Network
+
+/// A bidirectional data connection between a local endpoint and remote endpoint over socks proxy.
+final class Socks5Connection {
+ /// The remote endpoint to which the client wants to establish connection over the socks proxy.
+ let remoteServerEndpoint: Socks5Endpoint
+
+ /**
+ Initializes a new connection passing data between local and remote TCP connection over the socks proxy.
+
+ - Parameters:
+ - queue: the queue on which connection events are delivered.
+ - localConnection: the local TCP connection.
+ - socksProxyEndpoint: the socks proxy endpoint.
+ - remoteServerEndpoint: the remote endpoint to which the client wants to establish connection over the socks proxy.
+ */
+ init(
+ queue: DispatchQueue,
+ localConnection: NWConnection,
+ socksProxyEndpoint: NWEndpoint,
+ remoteServerEndpoint: Socks5Endpoint
+ ) {
+ self.queue = queue
+ self.remoteServerEndpoint = remoteServerEndpoint
+ self.localConnection = localConnection
+ self.remoteConnection = NWConnection(to: socksProxyEndpoint, using: .tcp)
+ }
+
+ /**
+ Start establishing a connection.
+
+ The start operation is asynchronous. Calls to start after the first one are ignored.
+ */
+ func start() {
+ queue.async { [self] in
+ guard case .initialized = state else { return }
+
+ state = .started
+
+ localConnection.stateUpdateHandler = onLocalConnectionState
+ remoteConnection.stateUpdateHandler = onRemoteConnectionState
+ localConnection.start(queue: queue)
+ remoteConnection.start(queue: queue)
+ }
+ }
+
+ /**
+ Cancel the connection.
+
+ Cancellation is asynchronous. All block handlers are released to break retain cycles once connection moved to stopped state. The object is not meant to be
+ reused or restarted after cancellation.
+
+ Calls to cancel after the first one are ignored.
+ */
+ func cancel() {
+ queue.async { [self] in
+ cancel(error: nil)
+ }
+ }
+
+ /**
+ Set a handler that receives connection state events.
+
+ It's advised to set the state handler before starting the connection to avoid missing updates to the connection state.
+
+ - Parameter newStateHandler: state handler block.
+ */
+ func setStateHandler(_ newStateHandler: ((Socks5Connection, State) -> Void)?) {
+ queue.async { [self] in
+ stateHandler = newStateHandler
+ }
+ }
+
+ // MARK: - Private
+
+ /// Connection state.
+ enum State {
+ /// Connection object is initialized. Default state.
+ case initialized
+
+ /// Connection is started.
+ case started
+
+ /// Connection to socks proxy is initiated.
+ case connectionInitiated
+
+ /// Connection object is in stopped state.
+ case stopped(Error?)
+
+ /// Returns `true` if connection is in `.stopped` state.
+ var isStopped: Bool {
+ if case .stopped = self {
+ return true
+ } else {
+ return false
+ }
+ }
+ }
+
+ private let queue: DispatchQueue
+ private let localConnection: NWConnection
+ private let remoteConnection: NWConnection
+ private var stateHandler: ((Socks5Connection, State) -> Void)?
+ private var state: State = .initialized {
+ didSet {
+ stateHandler?(self, state)
+ }
+ }
+
+ private func cancel(error: Error?) {
+ guard !state.isStopped else { return }
+
+ state = .stopped(error)
+ stateHandler = nil
+
+ localConnection.cancel()
+ remoteConnection.cancel()
+ }
+
+ private func onLocalConnectionState(_ connectionState: NWConnection.State) {
+ switch connectionState {
+ case .setup, .preparing, .cancelled:
+ break
+
+ case .ready:
+ initiateConnection()
+
+ case let .waiting(error), let .failed(error):
+ handleError(Socks5Error.localConnectionFailure(error))
+
+ @unknown default:
+ break
+ }
+ }
+
+ private func onRemoteConnectionState(_ connectionState: NWConnection.State) {
+ switch connectionState {
+ case .setup, .preparing, .cancelled:
+ break
+
+ case .ready:
+ initiateConnection()
+
+ case let .waiting(error), let .failed(error):
+ handleError(Socks5Error.remoteConnectionFailure(error))
+
+ @unknown default:
+ break
+ }
+ }
+
+ /// Initiate connection to socks proxy if local and remote connections are both ready.
+ /// Repeat calls to this method do nothing once connection to socks proxy is initiated.
+ private func initiateConnection() {
+ guard case .started = state else { return }
+ guard case (.ready, .ready) = (localConnection.state, remoteConnection.state) else { return }
+
+ state = .connectionInitiated
+ sendHandshake()
+ }
+
+ private func handleError(_ error: Error) {
+ cancel(error: error)
+ }
+
+ /// Start handshake with the socks proxy.
+ private func sendHandshake() {
+ let handshake = Socks5Handshake()
+ let negotiation = Socks5HandshakeNegotiation(
+ connection: remoteConnection,
+ handshake: handshake,
+ onComplete: onHandshake,
+ onFailure: handleError
+ )
+ negotiation.perform()
+ }
+
+ /// Handles handshake reply.
+ /// Initiates authentication flow if indicated in reply, otherwise starts connection negotiation immediately.
+ private func onHandshake(_ reply: Socks5HandshakeReply) {
+ switch reply.method {
+ case .notRequired:
+ connect()
+
+ case .usernamePassword:
+ // TODO: handle authentication
+ break
+ }
+ }
+
+ /// Start connection negotiation.
+ /// Upon successful negotiation, the client can begin exchanging data with remote server.
+ private func connect() {
+ let negotiation = Socks5ConnectNegotiation(
+ connection: remoteConnection,
+ endpoint: remoteServerEndpoint,
+ onComplete: { [self] reply in
+ if case .succeeded = reply.status {
+ stream()
+ } else {
+ handleError(Socks5Error.connectionRejected(reply.status))
+ }
+ },
+ onFailure: handleError
+ )
+ negotiation.perform()
+ }
+
+ /// Start streaming data between local and remote endpoint.
+ private func stream() {
+ let streamHandler = Socks5DataStreamHandler(
+ localConnection: localConnection,
+ remoteConnection: remoteConnection
+ ) { [self] error in
+ self.handleError(error)
+ }
+ streamHandler.start()
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift b/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift
new file mode 100644
index 0000000000..325c7527da
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift
@@ -0,0 +1,13 @@
+//
+// Socks5Constants.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+
+enum Socks5Constants {
+ /// Socks version.
+ static let socksVersion: UInt8 = 0x05
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift b/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift
new file mode 100644
index 0000000000..1089cecb8f
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift
@@ -0,0 +1,80 @@
+//
+// Socks5DataStreamHandler.swift
+// MullvadTransport
+//
+// Created by pronebird on 20/10/2023.
+//
+
+import Foundation
+import Network
+
+/// The object handling bidirectional streaming of data between local and remote connection.
+struct Socks5DataStreamHandler {
+ /// How many bytes the handler can receive at one time, when streaming data between local and remote connection.
+ static let maxBytesToRead = Int(UInt16.max)
+
+ /// Local TCP connection.
+ let localConnection: NWConnection
+
+ /// Remote TCP connection to the socks proxy.
+ let remoteConnection: NWConnection
+
+ /// Error handler.
+ let errorHandler: (Error) -> Void
+
+ /// Start streaming data between local and remote connection.
+ func start() {
+ streamOutboundTraffic()
+ streamInboundTraffic()
+ }
+
+ /// Pass outbound traffic from local to remote connection.
+ private func streamOutboundTraffic() {
+ localConnection.receive(
+ minimumIncompleteLength: 1,
+ maximumLength: Self.maxBytesToRead
+ ) { [self] content, _, isComplete, error in
+ if let error {
+ errorHandler(Socks5Error.localConnectionFailure(error))
+ return
+ }
+
+ remoteConnection.send(
+ content: content,
+ isComplete: isComplete,
+ completion: .contentProcessed { [self] error in
+ if let error {
+ errorHandler(Socks5Error.remoteConnectionFailure(error))
+ } else if !isComplete {
+ streamOutboundTraffic()
+ }
+ }
+ )
+ }
+ }
+
+ /// Pass inbound traffic from remote to local connection.
+ private func streamInboundTraffic() {
+ remoteConnection.receive(
+ minimumIncompleteLength: 1,
+ maximumLength: Self.maxBytesToRead
+ ) { [self] content, _, isComplete, error in
+ if let error {
+ errorHandler(Socks5Error.remoteConnectionFailure(error))
+ return
+ }
+
+ localConnection.send(
+ content: content,
+ isComplete: isComplete,
+ completion: .contentProcessed { [self] error in
+ if let error {
+ errorHandler(Socks5Error.localConnectionFailure(error))
+ } else if !isComplete {
+ streamInboundTraffic()
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift b/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift
new file mode 100644
index 0000000000..1587991bf7
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift
@@ -0,0 +1,111 @@
+//
+// Socks5Endpoint.swift
+// MullvadTransport
+//
+// Created by pronebird on 20/10/2023.
+//
+
+import Foundation
+import MullvadTypes
+import Network
+
+/// A network endpoint specified by DNS name and port.
+public struct Socks5HostEndpoint {
+ /// The endpoint's hostname.
+ public let hostname: String
+
+ /// The endpoint's port.
+ public let port: UInt16
+
+ /**
+ Initializes a new host endpoint.
+
+ Returns `nil` when the hostname is either empty or longer than 255 bytes, because it cannot be represented in socks protocol.
+
+ - Parameters:
+ - hostname: the endpoint's hostname
+ - port: the endpoint's port
+ */
+ public init?(hostname: String, port: UInt16) {
+ // The maximum length of domain name in bytes.
+ let maxHostnameLength = UInt8.max
+ let hostnameByteLength = Data(hostname.utf8).count
+
+ // Empty hostname is meaningless.
+ guard hostnameByteLength > 0 else { return nil }
+
+ // The length larger than 255 bytes cannot be represented in socks protocol.
+ guard hostnameByteLength <= maxHostnameLength else { return nil }
+
+ self.hostname = hostname
+ self.port = port
+ }
+}
+
+/// The endpoint type used by objects implementing socks protocol.
+public enum Socks5Endpoint {
+ /// IPv4 endpoint.
+ case ipv4(IPv4Endpoint)
+
+ /// IPv6 endpoint.
+ case ipv6(IPv6Endpoint)
+
+ /// Domain name endpoint.
+ case domain(Socks5HostEndpoint)
+
+ /// The corresponding raw socks address type.
+ var addressType: Socks5AddressType {
+ switch self {
+ case .ipv4:
+ return .ipv4
+ case .ipv6:
+ return .ipv6
+ case .domain:
+ return .domainName
+ }
+ }
+
+ /// The port associated with the underlying endpoint.
+ var port: UInt16 {
+ switch self {
+ case let .ipv4(endpoint):
+ endpoint.port
+ case let .ipv6(endpoint):
+ endpoint.port
+ case let .domain(endpoint):
+ endpoint.port
+ }
+ }
+
+ /// The byte representation in socks protocol.
+ var rawData: Data {
+ var data = Data()
+
+ switch self {
+ case let .ipv4(endpoint):
+ data.append(contentsOf: endpoint.ip.rawValue)
+
+ case let .ipv6(endpoint):
+ data.append(contentsOf: endpoint.ip.rawValue)
+
+ case let .domain(endpoint):
+ // Convert hostname to byte data without nul terminator.
+ let domainNameBytes = Data(endpoint.hostname.utf8)
+
+ // Append the length of domain name.
+ // Host endpoint already ensures that the length of domain name does not exceed the maximum value that
+ // single byte can hold.
+ data.append(UInt8(domainNameBytes.count))
+
+ // Append the domain name.
+ data.append(contentsOf: domainNameBytes)
+ }
+
+ // Append port in network byte order.
+ withUnsafeBytes(of: port.bigEndian) { buffer in
+ data.append(contentsOf: buffer)
+ }
+
+ return data
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift b/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift
new file mode 100644
index 0000000000..aff939e8be
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift
@@ -0,0 +1,124 @@
+//
+// Socks5EndpointReader.swift
+// MullvadTransport
+//
+// Created by pronebird on 21/10/2023.
+//
+
+import Foundation
+import MullvadTypes
+import Network
+
+/// The object reading the endpoint data from connection.
+struct Socks5EndpointReader {
+ /// Connection to the socks proxy.
+ let connection: NWConnection
+
+ /// The expected address type.
+ let addressType: Socks5AddressType
+
+ /// Completion handler called upon success.
+ let onComplete: (Socks5Endpoint) -> Void
+
+ /// Failure handler.
+ let onFailure: (Error) -> Void
+
+ /// Start reading endpoint from connection.
+ func perform() {
+ // The length of IPv4 address in bytes.
+ let ipv4AddressLength = 4
+
+ // The length of IPv6 address in bytes.
+ let ipv6AddressLength = 16
+
+ switch addressType {
+ case .ipv4:
+ readBoundAddressAndPortInner(addressLength: ipv4AddressLength)
+
+ case .ipv6:
+ readBoundAddressAndPortInner(addressLength: ipv6AddressLength)
+
+ case .domainName:
+ readBoundDomainNameLength { [self] domainLength in
+ readBoundAddressAndPortInner(addressLength: domainLength)
+ }
+ }
+ }
+
+ private func readBoundAddressAndPortInner(addressLength: Int) {
+ // The length of port in bytes.
+ let portLength = MemoryLayout<UInt16>.size
+
+ // The entire length of address + port
+ let byteSize = addressLength + portLength
+
+ connection.receive(exactLength: byteSize) { [self] addressData, _, _, error in
+ if let error {
+ onFailure(Socks5Error.remoteConnectionFailure(error))
+ } else if let addressData {
+ do {
+ let endpoint = try parseEndpoint(addressData: addressData, addressLength: addressLength)
+
+ onComplete(endpoint)
+ } catch {
+ onFailure(error)
+ }
+ } else {
+ onFailure(Socks5Error.unexpectedEndOfStream)
+ }
+ }
+ }
+
+ private func readBoundDomainNameLength(completion: @escaping (Int) -> Void) {
+ // The length of domain length parameter in bytes.
+ let domainLengthLength = MemoryLayout<UInt8>.size
+
+ connection.receive(exactLength: domainLengthLength) { [self] data, _, _, error in
+ if let error {
+ onFailure(Socks5Error.remoteConnectionFailure(error))
+ } else if let domainNameLength = data?.first {
+ completion(Int(domainNameLength))
+ } else {
+ onFailure(Socks5Error.unexpectedEndOfStream)
+ }
+ }
+ }
+
+ private func parseEndpoint(addressData: Data, addressLength: Int) throws -> Socks5Endpoint {
+ // The length of port in bytes.
+ let portLength = MemoryLayout<UInt16>.size
+
+ guard addressData.count == addressLength + portLength else { throw Socks5Error.unexpectedEndOfStream }
+
+ // Read address bytes.
+ let addressBytes = addressData[0 ..< addressLength]
+
+ // Read port bytes.
+ let port = addressData[addressLength...].withUnsafeBytes { buffer in
+ let value = buffer.load(as: UInt16.self)
+
+ // Port is passed in network byte order. Convert it to host order.
+ return UInt16(bigEndian: value)
+ }
+
+ // Parse address into endpoint.
+ switch addressType {
+ case .ipv4:
+ guard let ipAddress = IPv4Address(addressBytes) else { throw Socks5Error.parseIPv4Address }
+
+ return .ipv4(IPv4Endpoint(ip: ipAddress, port: port))
+
+ case .ipv6:
+ guard let ipAddress = IPv6Address(addressBytes) else { throw Socks5Error.parseIPv6Address }
+
+ return .ipv6(IPv6Endpoint(ip: ipAddress, port: port))
+
+ case .domainName:
+ guard let hostname = String(bytes: addressBytes, encoding: .utf8),
+ let endpoint = Socks5HostEndpoint(hostname: hostname, port: port) else {
+ throw Socks5Error.decodeDomainName
+ }
+ return .domain(endpoint)
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift
new file mode 100644
index 0000000000..992c402160
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift
@@ -0,0 +1,54 @@
+//
+// Socks5Error.swift
+// MullvadTransport
+//
+// Created by pronebird on 21/10/2023.
+//
+
+import Foundation
+import Network
+
+/// The errors returned by objects implementing socks proxy.
+public enum Socks5Error: Error {
+ /// Unexpected end of stream.
+ case unexpectedEndOfStream
+
+ /// Failure to decode the domain name from byte stream into utf8 string.
+ case decodeDomainName
+
+ /// Failure to parse IPv4 address from raw data.
+ case parseIPv4Address
+
+ /// Failure to parse IPv6 address from raw data.
+ case parseIPv6Address
+
+ /// Server replied with invalid socks version.
+ case invalidSocksVersion
+
+ /// Server replied with unknown endpoint address type.
+ case invalidAddressType
+
+ /// Invalid (unassigned) status code is returned.
+ case invalidStatusCode(UInt8)
+
+ /// Server replied with unsupported authentication method.
+ case unsupportedAuthMethod
+
+ /// None of the auth methods listed by the client are acceptable.
+ case unacceptableAuthMethods
+
+ /// Connection request is rejected.
+ case connectionRejected(Socks5StatusCode)
+
+ /// Failure to instantiate a TCP listener.
+ case createTcpListener(Error)
+
+ /// Socks forwarding proxy was cancelled during startup.
+ case cancelledDuringStartup
+
+ /// Local connection failure.
+ case localConnectionFailure(NWError)
+
+ /// Remote connection failure.
+ case remoteConnectionFailure(NWError)
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift
new file mode 100644
index 0000000000..0b26bc3b92
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift
@@ -0,0 +1,281 @@
+//
+// Socks5ForwardingProxy.swift
+// MullvadTransport
+//
+// Created by pronebird on 18/10/2023.
+//
+
+import Foundation
+import Network
+
+/**
+ The proxy that can forward data connection from local TCP port to remote TCP server over the socks proxy.
+
+ The forwarding socks proxy acts as a transparent proxy. The HTTP/S clients that don't support proxy configuration can be configured to direct their traffic at the
+ local TCP port opened by the forwarding socks proxy.
+
+ The forwarding proxy then takes care of negotiating with the remote socks proxy and transparently handles all traffic as if the HTTP/S client talks directly to the remote
+ server.
+
+ Refer to RFC1928 for more info on socks5: <https://datatracker.ietf.org/doc/html/rfc1928>
+ */
+public final class Socks5ForwardingProxy {
+ /// Socks proxy endpoint.
+ public let socksProxyEndpoint: NWEndpoint
+
+ /// Remote server that socks proxy should connect to.
+ public let remoteServerEndpoint: Socks5Endpoint
+
+ /// Local TCP port that clients should use to communicate with the remote server.
+ /// This property is set once the proxy is successfully started.
+ public var listenPort: UInt16? {
+ queue.sync {
+ switch state {
+ case let .started(listener, _):
+ return listener.port?.rawValue
+ case .stopped, .starting:
+ return nil
+ }
+ }
+ }
+
+ /**
+ Initializes a socks forwarding proxy accepting connections on local TCP port and establishing connection to the remote endpoint over socks proxy.
+
+ - Parameters:
+ - socksProxyEndpoint: socks proxy endpoint.
+ - remoteServerEndpoint: remote server that socks proxy should connect to.
+ */
+ public init(socksProxyEndpoint: NWEndpoint, remoteServerEndpoint: Socks5Endpoint) {
+ self.socksProxyEndpoint = socksProxyEndpoint
+ self.remoteServerEndpoint = remoteServerEndpoint
+ }
+
+ deinit {
+ stopInner()
+ }
+
+ /**
+ Start forwarding proxy.
+
+ Repeat calls do nothing, but accumulate the completion handler for invocation once the proxy moves to the next state.
+
+ - Parameter completion: completion handler that is called once the TCP listener is ready in the first time or failed before moving to the ready state.
+ Invoked on main queue.
+ */
+ public func start(completion: @escaping (Error?) -> Void) {
+ queue.async {
+ self.startListener { error in
+ DispatchQueue.main.async {
+ completion(error)
+ }
+ }
+ }
+ }
+
+ /**
+ Stop forwarding proxy.
+
+ - Parameter completion: completion handler that's called immediately after cancelling the TCP listener. Invoked on main queue.
+ */
+ public func stop(completion: (() -> Void)? = nil) {
+ queue.async {
+ self.stopInner()
+
+ DispatchQueue.main.async {
+ completion?()
+ }
+ }
+ }
+
+ /**
+ Set error handler to receive unrecoverable errors at runtime.
+
+ - Parameter errorHandler: an error handler block. Invoked on main queue.
+ */
+ public func setErrorHandler(_ errorHandler: ((Error) -> Void)?) {
+ queue.async {
+ self.errorHandler = errorHandler
+ }
+ }
+
+ // MARK: - Private
+
+ private enum State {
+ /// Proxy is starting up.
+ case starting(listener: NWListener, completion: (Error?) -> Void)
+
+ /// Proxy is ready.
+ case started(listener: NWListener, openConnections: [Socks5Connection])
+
+ /// Proxy is not running.
+ case stopped
+ }
+
+ private let queue = DispatchQueue(label: "Socks5ForwardingProxy-queue")
+ private var state: State = .stopped
+ private var errorHandler: ((Error) -> Void)?
+
+ /**
+ Start TCP listener.
+
+ - Parameter completion: completion handler that is called once the TCP listener is ready or failed.
+ */
+ private func startListener(completion: @escaping (Error?) -> Void) {
+ switch state {
+ case .started:
+ completion(nil)
+
+ case let .starting(listener, previousCompletion):
+ // Accumulate completion handlers when requested to start multiple times in a row.
+ self.state = .starting(listener: listener, completion: { error in
+ previousCompletion(error)
+ completion(error)
+ })
+
+ case .stopped:
+ do {
+ let tcpListener = try makeTCPListener()
+ state = .starting(listener: tcpListener, completion: completion)
+ tcpListener.start(queue: queue)
+ } catch {
+ completion(Socks5Error.createTcpListener(error))
+ }
+ }
+ }
+
+ /**
+ Create new TCP listener.
+
+ - Throws: an instance of `NWError` if unable to initialize `NWListener`.
+ - Returns: a configured instance of `NWListener`.
+ */
+ private func makeTCPListener() throws -> NWListener {
+ let tcpListener = try NWListener(using: .tcp)
+ tcpListener.stateUpdateHandler = { [weak self] state in
+ self?.onListenerState(state)
+ }
+ tcpListener.newConnectionHandler = { [weak self] connection in
+ self?.onNewConnection(connection)
+ }
+ return tcpListener
+ }
+
+ /**
+ Reset block handlers and cancel an instance of `NWListener`.
+
+ - Parameter tcpListener: an instance of `NWListener`.
+ */
+ private func cancelListener(_ tcpListener: NWListener) {
+ tcpListener.stateUpdateHandler = nil
+ tcpListener.newConnectionHandler = nil
+ tcpListener.cancel()
+ }
+
+ private func stopInner() {
+ switch state {
+ case let .starting(listener, completion):
+ state = .stopped
+ cancelListener(listener)
+ DispatchQueue.main.async {
+ completion(Socks5Error.cancelledDuringStartup)
+ }
+
+ case let .started(listener, openConnections):
+ state = .stopped
+ cancelListener(listener)
+ openConnections.forEach { $0.cancel() }
+
+ case .stopped:
+ break
+ }
+ }
+
+ private func onReady() {
+ switch state {
+ case let .starting(listener, completion):
+ state = .started(listener: listener, openConnections: [])
+
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+
+ case .started, .stopped:
+ break
+ }
+ }
+
+ private func onFailure(_ error: Error) {
+ switch state {
+ case let .starting(_, completion):
+ state = .stopped
+
+ DispatchQueue.main.async {
+ completion(error)
+ }
+
+ case .started:
+ state = .stopped
+ DispatchQueue.main.async {
+ self.errorHandler?(error)
+ }
+
+ case .stopped:
+ break
+ }
+ }
+
+ private func onListenerState(_ listenerState: NWListener.State) {
+ switch listenerState {
+ case .setup, .cancelled:
+ break
+
+ case .ready:
+ onReady()
+
+ case let .failed(error), let .waiting(error):
+ onFailure(error)
+
+ @unknown default:
+ break
+ }
+ }
+
+ private func onNewConnection(_ connection: NWConnection) {
+ switch state {
+ case .starting, .stopped:
+ connection.cancel()
+
+ case .started(let listener, var openConnections):
+ let socks5Connection = Socks5Connection(
+ queue: queue,
+ localConnection: connection,
+ socksProxyEndpoint: socksProxyEndpoint,
+ remoteServerEndpoint: remoteServerEndpoint
+ )
+ socks5Connection.setStateHandler { [weak self] socks5Connection, state in
+ if case let .stopped(error) = state {
+ self?.onEndConnection(socks5Connection, error: error)
+ }
+ }
+
+ openConnections.append(socks5Connection)
+ state = .started(listener: listener, openConnections: openConnections)
+
+ socks5Connection.start()
+ }
+ }
+
+ private func onEndConnection(_ connection: Socks5Connection, error: Error?) {
+ switch state {
+ case .stopped, .starting:
+ break
+
+ case .started(let listener, var openConnections):
+ guard let index = openConnections.firstIndex(where: { $0 === connection }) else { return }
+
+ openConnections.remove(at: index)
+ state = .started(listener: listener, openConnections: openConnections)
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Handshake.swift b/ios/MullvadREST/Transport/Socks5/Socks5Handshake.swift
new file mode 100644
index 0000000000..23fadc64fd
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Handshake.swift
@@ -0,0 +1,43 @@
+//
+// Socks5Handshake.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+
+/// Handshake initiation message.
+struct Socks5Handshake {
+ /// Authentication methods supported by the client.
+ /// Defaults to `.notRequired` when empty.
+ var methods: [Socks5AuthenticationMethod] = []
+
+ /// The byte representation in socks protocol.
+ var rawData: Data {
+ var data = Data()
+ var methods = methods
+
+ // Make sure to provide at least one supported authentication method.
+ if methods.isEmpty {
+ methods.append(.notRequired)
+ }
+
+ // Append socks version
+ data.append(Socks5Constants.socksVersion)
+
+ // Append number of suppported authentication methods supported.
+ data.append(UInt8(methods.count))
+
+ // Append authentication methods
+ data.append(contentsOf: methods.map { $0.rawValue })
+
+ return data
+ }
+}
+
+/// Handshake reply message.
+struct Socks5HandshakeReply {
+ /// The authentication method accepted by the socks proxys.
+ var method: Socks5AuthenticationMethod
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift b/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift
new file mode 100644
index 0000000000..7b1059de1a
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift
@@ -0,0 +1,68 @@
+//
+// Socks5HandshakeNegotiation.swift
+// MullvadTransport
+//
+// Created by pronebird on 20/10/2023.
+//
+
+import Foundation
+import Network
+
+/// The object handling a handshake negotiation with socks proxy.
+struct Socks5HandshakeNegotiation {
+ let connection: NWConnection
+ let handshake: Socks5Handshake
+ let onComplete: (Socks5HandshakeReply) -> Void
+ let onFailure: (Error) -> Void
+
+ func perform() {
+ connection.send(content: handshake.rawData, completion: .contentProcessed { [self] error in
+ if let error {
+ onFailure(Socks5Error.remoteConnectionFailure(error))
+ } else {
+ readReply()
+ }
+ })
+ }
+
+ private func readReply() {
+ // The length of a handshake reply in bytes.
+ let replyLength = 2
+
+ connection.receive(exactLength: replyLength) { [self] data, _, _, error in
+ if let error {
+ onFailure(Socks5Error.remoteConnectionFailure(error))
+ } else if let data {
+ do {
+ onComplete(try parseReply(data: data))
+ } catch {
+ onFailure(error)
+ }
+ } else {
+ onFailure(Socks5Error.unexpectedEndOfStream)
+ }
+ }
+ }
+
+ private func parseReply(data: Data) throws -> Socks5HandshakeReply {
+ var iterator = data.makeIterator()
+
+ guard let version = iterator.next() else { throw Socks5Error.unexpectedEndOfStream }
+ guard version == Socks5Constants.socksVersion else { throw Socks5Error.invalidSocksVersion }
+
+ guard let rawMethod = iterator.next() else { throw Socks5Error.unexpectedEndOfStream }
+
+ // The response code returned by the server when none of the auth methods listed by the client are acceptable.
+ let authMethodsUnacceptableReplyCode: UInt8 = 0xff
+
+ guard rawMethod != authMethodsUnacceptableReplyCode else {
+ throw Socks5Error.unacceptableAuthMethods
+ }
+
+ guard let method = Socks5AuthenticationMethod(rawValue: rawMethod) else {
+ throw Socks5Error.unsupportedAuthMethod
+ }
+
+ return Socks5HandshakeReply(method: method)
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift b/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift
new file mode 100644
index 0000000000..9832c156a1
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift
@@ -0,0 +1,21 @@
+//
+// Socks5StatusCode.swift
+// MullvadTransport
+//
+// Created by pronebird on 19/10/2023.
+//
+
+import Foundation
+
+/// Status code used in socks protocol.
+public enum Socks5StatusCode: UInt8 {
+ case succeeded = 0x00
+ case failure = 0x01
+ case connectionNotAllowedByRuleset = 0x02
+ case networkUnreachable = 0x03
+ case hostUnreachable = 0x04
+ case connectionRefused = 0x05
+ case ttlExpired = 0x06
+ case commandNotSupported = 0x07
+ case addressTypeNotSupported = 0x08
+}
diff --git a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift
new file mode 100644
index 0000000000..f1075692e6
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift
@@ -0,0 +1,112 @@
+//
+// URLSessionSocks5Transport.swift
+// MullvadTransport
+//
+// Created by pronebird on 23/10/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadTypes
+
+/// Transport that passes URL requests over the local socks forwarding proxy.
+public class URLSessionSocks5Transport: RESTTransport {
+ /// Socks5 forwarding proxy.
+ private let socksProxy: Socks5ForwardingProxy
+
+ /// The IPv4 representation of the loopback address used by `socksProxy`.
+ private let localhost = "127.0.0.1"
+
+ /// The `URLSession` used to send requests via `socksProxy`.
+ public let urlSession: URLSession
+
+ public var name: String {
+ "socks5-url-session"
+ }
+
+ private let logger = Logger(label: "URLSessionSocks5Transport")
+
+ /**
+ Instantiates new socks5 transport.
+
+ - Parameters:
+ - urlSession: an instance of URLSession used for sending requests.
+ - configuration: SOCKS5 configuration
+ - addressCache: an address cache
+ */
+ public init(
+ urlSession: URLSession,
+ configuration: Socks5Configuration,
+ addressCache: REST.AddressCache
+ ) {
+ self.urlSession = urlSession
+
+ let apiAddress = addressCache.getCurrentEndpoint()
+
+ socksProxy = Socks5ForwardingProxy(
+ socksProxyEndpoint: configuration.proxyEndpoint.nwEndpoint,
+ remoteServerEndpoint: apiAddress.socksEndpoint
+ )
+
+ socksProxy.setErrorHandler { [weak self] error in
+ self?.logger.error(error: error, message: "Socks proxy failed at runtime.")
+ }
+ }
+
+ public func sendRequest(
+ _ request: URLRequest,
+ completion: @escaping (Data?, URLResponse?, Error?) -> Void
+ ) -> Cancellable {
+ // Listen port should be set when socks proxy is ready. Otherwise start proxy and only then start the data task.
+ if let localPort = socksProxy.listenPort {
+ return startDataTask(request: request, localPort: localPort, completion: completion)
+ } else {
+ return sendDeferred(request: request, completion: completion)
+ }
+ }
+
+ /// Starts socks proxy then executes the data task.
+ private func sendDeferred(
+ request: URLRequest,
+ completion: @escaping (Data?, URLResponse?, Error?) -> Void
+ ) -> Cancellable {
+ let chain = CancellableChain()
+
+ socksProxy.start { [weak self, weak socksProxy] error in
+ if let error {
+ completion(nil, nil, error)
+ } else if let self, let localPort = socksProxy?.listenPort {
+ let token = self.startDataTask(request: request, localPort: localPort, completion: completion)
+
+ // Propagate cancellation from the chain to the data task cancellation token.
+ chain.link(token)
+ } else {
+ completion(nil, nil, URLError(.cancelled))
+ }
+ }
+
+ return chain
+ }
+
+ /// Execute data task, rewriting the original URLRequest to communicate over the socks proxy listening on the local TCP port.
+ private func startDataTask(
+ request: URLRequest,
+ localPort: UInt16,
+ completion: @escaping (Data?, URLResponse?, Error?) -> Void
+ ) -> Cancellable {
+ // Copy the URL request and rewrite the host and port to point to the socks5 forwarding proxy instance
+ var newRequest = request
+
+ newRequest.url = request.url.flatMap { url in
+ var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
+ components?.host = localhost
+ components?.port = Int(localPort)
+ return components?.url
+ }
+
+ let dataTask = urlSession.dataTask(with: newRequest, completionHandler: completion)
+ dataTask.resume()
+ return dataTask
+ }
+}
diff --git a/ios/MullvadREST/Transport/TransportProvider.swift b/ios/MullvadREST/Transport/TransportProvider.swift
index 65ff2b3209..1bd629d79b 100644
--- a/ios/MullvadREST/Transport/TransportProvider.swift
+++ b/ios/MullvadREST/Transport/TransportProvider.swift
@@ -78,6 +78,17 @@ public final class TransportProvider: RESTTransportProvider {
}
}
+ private func socks5() -> RESTTransport? {
+ return URLSessionSocks5Transport(
+ urlSession: urlSessionTransport.urlSession,
+ configuration: Socks5Configuration(proxyEndpoint: AnyIPEndpoint.ipv4(IPv4Endpoint(
+ ip: .loopback,
+ port: 8889
+ ))),
+ addressCache: addressCache
+ )
+ }
+
/// Returns the last used shadowsocks configuration, otherwise a new randomized configuration.
private func shadowsocksConfiguration() throws -> ShadowsocksConfiguration {
// If a previous shadowsocks configuration was in cache, return it directly.
@@ -147,6 +158,8 @@ public final class TransportProvider: RESTTransportProvider {
currentTransport = shadowsocks()
case .useURLSession:
currentTransport = urlSessionTransport
+ case .useSocks5:
+ currentTransport = socks5()
}
}
return currentTransport
diff --git a/ios/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift
index d857a3406b..27411d244e 100644
--- a/ios/MullvadREST/Transport/TransportStrategy.swift
+++ b/ios/MullvadREST/Transport/TransportStrategy.swift
@@ -15,6 +15,8 @@ public struct TransportStrategy: Equatable {
case useURLSession
/// Suggests connecting via Shadowsocks proxy
case useShadowsocks
+ /// Suggests connecting via socks proxy
+ case useSocks5
}
/// The internal counter for suggested transports.
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 6eed2f7b93..9e833a8ec8 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -489,6 +489,26 @@
A900E9BC2ACC609200C95F67 /* DevicesProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */; };
A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */; };
A900E9C02ACC661900C95F67 /* AccessTokenManager+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */; };
+ A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */; };
+ A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A12B2857D50045ADF0 /* Socks5Endpoint.swift */; };
+ A90763B22B2857D50045ADF0 /* Socks5EndpointReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A22B2857D50045ADF0 /* Socks5EndpointReader.swift */; };
+ A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A32B2857D50045ADF0 /* Socks5Authentication.swift */; };
+ A90763B42B2857D50045ADF0 /* NWConnection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A42B2857D50045ADF0 /* NWConnection+Extensions.swift */; };
+ A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A52B2857D50045ADF0 /* Socks5Constants.swift */; };
+ A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift */; };
+ A90763B72B2857D50045ADF0 /* Socks5DataStreamHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A72B2857D50045ADF0 /* Socks5DataStreamHandler.swift */; };
+ A90763B82B2857D50045ADF0 /* Socks5Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A82B2857D50045ADF0 /* Socks5Command.swift */; };
+ A90763B92B2857D50045ADF0 /* Socks5ForwardingProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A92B2857D50045ADF0 /* Socks5ForwardingProxy.swift */; };
+ A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763AA2B2857D50045ADF0 /* Socks5Error.swift */; };
+ A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763AB2B2857D50045ADF0 /* Socks5AddressType.swift */; };
+ A90763BC2B2857D50045ADF0 /* Socks5StatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763AC2B2857D50045ADF0 /* Socks5StatusCode.swift */; };
+ A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763AD2B2857D50045ADF0 /* Socks5Connection.swift */; };
+ A90763BE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763AE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift */; };
+ A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763AF2B2857D50045ADF0 /* Socks5Handshake.swift */; };
+ A90763C12B2858320045ADF0 /* URLSessionSocks5Transport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763C02B2858310045ADF0 /* URLSessionSocks5Transport.swift */; };
+ A90763C32B2858630045ADF0 /* Socks5Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763C22B2858630045ADF0 /* Socks5Configuration.swift */; };
+ A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763C42B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift */; };
+ A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763C62B2858DC0045ADF0 /* CancellableChain.swift */; };
A91614D12B108D1B00F416EB /* TransportLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91614D02B108D1B00F416EB /* TransportLayer.swift */; };
A91614D42B108F5600F416EB /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; };
A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */; };
@@ -1518,6 +1538,26 @@
A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DevicesProxy+Stubs.swift"; sourceTree = "<group>"; };
A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIProxy+Stubs.swift"; sourceTree = "<group>"; };
A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenManager+Stubs.swift"; sourceTree = "<group>"; };
+ A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectCommand.swift; sourceTree = "<group>"; };
+ A90763A12B2857D50045ADF0 /* Socks5Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Endpoint.swift; sourceTree = "<group>"; };
+ A90763A22B2857D50045ADF0 /* Socks5EndpointReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5EndpointReader.swift; sourceTree = "<group>"; };
+ A90763A32B2857D50045ADF0 /* Socks5Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Authentication.swift; sourceTree = "<group>"; };
+ A90763A42B2857D50045ADF0 /* NWConnection+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWConnection+Extensions.swift"; sourceTree = "<group>"; };
+ A90763A52B2857D50045ADF0 /* Socks5Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Constants.swift; sourceTree = "<group>"; };
+ A90763A62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectNegotiation.swift; sourceTree = "<group>"; };
+ A90763A72B2857D50045ADF0 /* Socks5DataStreamHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5DataStreamHandler.swift; sourceTree = "<group>"; };
+ A90763A82B2857D50045ADF0 /* Socks5Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Command.swift; sourceTree = "<group>"; };
+ A90763A92B2857D50045ADF0 /* Socks5ForwardingProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ForwardingProxy.swift; sourceTree = "<group>"; };
+ A90763AA2B2857D50045ADF0 /* Socks5Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Error.swift; sourceTree = "<group>"; };
+ A90763AB2B2857D50045ADF0 /* Socks5AddressType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5AddressType.swift; sourceTree = "<group>"; };
+ A90763AC2B2857D50045ADF0 /* Socks5StatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5StatusCode.swift; sourceTree = "<group>"; };
+ A90763AD2B2857D50045ADF0 /* Socks5Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Connection.swift; sourceTree = "<group>"; };
+ A90763AE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5HandshakeNegotiation.swift; sourceTree = "<group>"; };
+ A90763AF2B2857D50045ADF0 /* Socks5Handshake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Handshake.swift; sourceTree = "<group>"; };
+ A90763C02B2858310045ADF0 /* URLSessionSocks5Transport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionSocks5Transport.swift; sourceTree = "<group>"; };
+ A90763C22B2858630045ADF0 /* Socks5Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Configuration.swift; sourceTree = "<group>"; };
+ A90763C42B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AnyIPEndpoint+Socks5.swift"; sourceTree = "<group>"; };
+ A90763C62B2858DC0045ADF0 /* CancellableChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancellableChain.swift; sourceTree = "<group>"; };
A91614D02B108D1B00F416EB /* TransportLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportLayer.swift; sourceTree = "<group>"; };
A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelControlViewModel.swift; sourceTree = "<group>"; };
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
@@ -2868,6 +2908,33 @@
path = RelayFilter;
sourceTree = "<group>";
};
+ A907639F2B2857D50045ADF0 /* Socks5 */ = {
+ isa = PBXGroup;
+ children = (
+ A90763C42B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift */,
+ A90763C62B2858DC0045ADF0 /* CancellableChain.swift */,
+ A90763A42B2857D50045ADF0 /* NWConnection+Extensions.swift */,
+ A90763AB2B2857D50045ADF0 /* Socks5AddressType.swift */,
+ A90763A32B2857D50045ADF0 /* Socks5Authentication.swift */,
+ A90763A82B2857D50045ADF0 /* Socks5Command.swift */,
+ A90763C22B2858630045ADF0 /* Socks5Configuration.swift */,
+ A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */,
+ A90763AD2B2857D50045ADF0 /* Socks5Connection.swift */,
+ A90763A62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift */,
+ A90763A52B2857D50045ADF0 /* Socks5Constants.swift */,
+ A90763A72B2857D50045ADF0 /* Socks5DataStreamHandler.swift */,
+ A90763A12B2857D50045ADF0 /* Socks5Endpoint.swift */,
+ A90763A22B2857D50045ADF0 /* Socks5EndpointReader.swift */,
+ A90763AA2B2857D50045ADF0 /* Socks5Error.swift */,
+ A90763A92B2857D50045ADF0 /* Socks5ForwardingProxy.swift */,
+ A90763AF2B2857D50045ADF0 /* Socks5Handshake.swift */,
+ A90763AE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift */,
+ A90763AC2B2857D50045ADF0 /* Socks5StatusCode.swift */,
+ A90763C02B2858310045ADF0 /* URLSessionSocks5Transport.swift */,
+ );
+ path = Socks5;
+ sourceTree = "<group>";
+ };
F028A5472A336E1900C0CAA3 /* RedeemVoucher */ = {
isa = PBXGroup;
children = (
@@ -2939,6 +3006,7 @@
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */,
58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */,
F0DC77A22B2314EF0087F09D /* Shadowsocks */,
+ A907639F2B2857D50045ADF0 /* Socks5 */,
F0DDE4112B220458006B57A7 /* TransportProvider.swift */,
A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */,
);
@@ -3836,8 +3904,11 @@
F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */,
F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */,
06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */,
+ A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */,
+ A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */,
06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */,
06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */,
+ A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */,
06799AEC28F98E4800ACD94E /* RESTTaskIdentifier.swift in Sources */,
58E7BA192A975DF70068EC3A /* RESTTransportProvider.swift in Sources */,
06799ADE28F98E4800ACD94E /* RESTRequestHandler.swift in Sources */,
@@ -3845,31 +3916,48 @@
06799AEF28F98E4800ACD94E /* RetryStrategy.swift in Sources */,
06799AE128F98E4800ACD94E /* SSLPinningURLSessionDelegate.swift in Sources */,
A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */,
+ A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */,
+ A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */,
F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */,
F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */,
06799AEA28F98E4800ACD94E /* RESTProxy.swift in Sources */,
+ A90763BC2B2857D50045ADF0 /* Socks5StatusCode.swift in Sources */,
+ A90763B82B2857D50045ADF0 /* Socks5Command.swift in Sources */,
+ A90763BE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift in Sources */,
+ A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */,
06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */,
+ A90763B92B2857D50045ADF0 /* Socks5ForwardingProxy.swift in Sources */,
+ A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */,
06799ADB28F98E4800ACD94E /* RESTProxyFactory.swift in Sources */,
F0DDE4182B220458006B57A7 /* ShadowsocksConfiguration.swift in Sources */,
06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */,
+ A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */,
06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */,
F0DDE4142B220458006B57A7 /* ShadowSocksProxy.swift in Sources */,
+ A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */,
F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */,
06799AE628F98E4800ACD94E /* ServerRelaysResponse.swift in Sources */,
F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */,
F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */,
+ A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */,
06799AF128F98E4800ACD94E /* RESTAPIProxy.swift in Sources */,
F0DDE42A2B220A15006B57A7 /* Haversine.swift in Sources */,
589E76C02A9378F100E502F3 /* RESTRequestExecutor.swift in Sources */,
+ A90763C12B2858320045ADF0 /* URLSessionSocks5Transport.swift in Sources */,
06799AE528F98E4800ACD94E /* HTTP.swift in Sources */,
A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */,
+ A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */,
06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */,
+ A90763B72B2857D50045ADF0 /* Socks5DataStreamHandler.swift in Sources */,
+ A90763B22B2857D50045ADF0 /* Socks5EndpointReader.swift in Sources */,
+ A90763B42B2857D50045ADF0 /* NWConnection+Extensions.swift in Sources */,
F06045EA2B23217E00B2D37A /* ShadowsocksTransport.swift in Sources */,
06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */,
06799AF028F98E4800ACD94E /* REST.swift in Sources */,
06799ADF28F98E4800ACD94E /* RESTDevicesProxy.swift in Sources */,
06799ADA28F98E4800ACD94E /* RESTResponseHandler.swift in Sources */,
062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */,
+ A90763C32B2858630045ADF0 /* Socks5Configuration.swift in Sources */,
06799AE428F98E4800ACD94E /* RESTAccountsProxy.swift in Sources */,
5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */,
06799AE328F98E4800ACD94E /* RESTNetworkOperation.swift in Sources */,