From acc777d02bb0748fe94391b9ec4eea530c382731 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Mon, 23 Oct 2023 17:12:55 +0200 Subject: [PATCH] Add socks5 forwarding proxy --- .../Direct/URLSessionTransport.swift | 2 +- .../Socks5/AnyIPEndpoint+Socks5.swift | 33 ++ .../Transport/Socks5/CancellableChain.swift | 43 +++ .../Socks5/NWConnection+Extensions.swift | 22 ++ .../Transport/Socks5/Socks5AddressType.swift | 15 + .../Socks5/Socks5Authentication.swift | 14 + .../Transport/Socks5/Socks5Command.swift | 15 + .../Socks5/Socks5Configuration.swift | 21 ++ .../Socks5/Socks5ConnectCommand.swift | 46 +++ .../Socks5/Socks5ConnectNegotiation.swift | 109 +++++++ .../Transport/Socks5/Socks5Connection.swift | 227 ++++++++++++++ .../Transport/Socks5/Socks5Constants.swift | 13 + .../Socks5/Socks5DataStreamHandler.swift | 80 +++++ .../Transport/Socks5/Socks5Endpoint.swift | 111 +++++++ .../Socks5/Socks5EndpointReader.swift | 124 ++++++++ .../Transport/Socks5/Socks5Error.swift | 54 ++++ .../Socks5/Socks5ForwardingProxy.swift | 281 ++++++++++++++++++ .../Transport/Socks5/Socks5Handshake.swift | 43 +++ .../Socks5/Socks5HandshakeNegotiation.swift | 68 +++++ .../Transport/Socks5/Socks5StatusCode.swift | 21 ++ .../Socks5/URLSessionSocks5Transport.swift | 112 +++++++ .../Transport/TransportProvider.swift | 13 + .../Transport/TransportStrategy.swift | 2 + ios/MullvadVPN.xcodeproj/project.pbxproj | 88 ++++++ 24 files changed, 1556 insertions(+), 1 deletion(-) create mode 100644 ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift create mode 100644 ios/MullvadREST/Transport/Socks5/CancellableChain.swift create mode 100644 ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Command.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5ConnectCommand.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Connection.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Constants.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Error.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5Handshake.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift create mode 100644 ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift create mode 100644 ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift diff --git a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift index fc5aee683b18..77c5b48e5981 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 000000000000..986f8276fab3 --- /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 000000000000..510d76094a4c --- /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 000000000000..d99f9e808185 --- /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 000000000000..ad013d1ae966 --- /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 000000000000..39a240d41ef7 --- /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 000000000000..293acf218b10 --- /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 000000000000..39033821d459 --- /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 000000000000..0ad4d23bbdaa --- /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 000000000000..cbd4f0875e13 --- /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 000000000000..a3163029fed2 --- /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 000000000000..325c7527da77 --- /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 000000000000..1089cecb8ff8 --- /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 000000000000..1587991bf7ce --- /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 000000000000..aff939e8be54 --- /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.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.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.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 000000000000..992c402160f9 --- /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 000000000000..0b26bc3b92ec --- /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: + */ +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 000000000000..23fadc64fd45 --- /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 000000000000..7b1059de1ae4 --- /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 000000000000..9832c156a13a --- /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 000000000000..f1075692e602 --- /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 65ff2b32096b..1bd629d79b16 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 d857a3406bde..27411d244e55 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 6eed2f7b9306..9e833a8ec805 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 = ""; }; A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIProxy+Stubs.swift"; sourceTree = ""; }; A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenManager+Stubs.swift"; sourceTree = ""; }; + A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectCommand.swift; sourceTree = ""; }; + A90763A12B2857D50045ADF0 /* Socks5Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Endpoint.swift; sourceTree = ""; }; + A90763A22B2857D50045ADF0 /* Socks5EndpointReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5EndpointReader.swift; sourceTree = ""; }; + A90763A32B2857D50045ADF0 /* Socks5Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Authentication.swift; sourceTree = ""; }; + A90763A42B2857D50045ADF0 /* NWConnection+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWConnection+Extensions.swift"; sourceTree = ""; }; + A90763A52B2857D50045ADF0 /* Socks5Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Constants.swift; sourceTree = ""; }; + A90763A62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectNegotiation.swift; sourceTree = ""; }; + A90763A72B2857D50045ADF0 /* Socks5DataStreamHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5DataStreamHandler.swift; sourceTree = ""; }; + A90763A82B2857D50045ADF0 /* Socks5Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Command.swift; sourceTree = ""; }; + A90763A92B2857D50045ADF0 /* Socks5ForwardingProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ForwardingProxy.swift; sourceTree = ""; }; + A90763AA2B2857D50045ADF0 /* Socks5Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Error.swift; sourceTree = ""; }; + A90763AB2B2857D50045ADF0 /* Socks5AddressType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5AddressType.swift; sourceTree = ""; }; + A90763AC2B2857D50045ADF0 /* Socks5StatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5StatusCode.swift; sourceTree = ""; }; + A90763AD2B2857D50045ADF0 /* Socks5Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Connection.swift; sourceTree = ""; }; + A90763AE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5HandshakeNegotiation.swift; sourceTree = ""; }; + A90763AF2B2857D50045ADF0 /* Socks5Handshake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Handshake.swift; sourceTree = ""; }; + A90763C02B2858310045ADF0 /* URLSessionSocks5Transport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionSocks5Transport.swift; sourceTree = ""; }; + A90763C22B2858630045ADF0 /* Socks5Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Configuration.swift; sourceTree = ""; }; + A90763C42B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AnyIPEndpoint+Socks5.swift"; sourceTree = ""; }; + A90763C62B2858DC0045ADF0 /* CancellableChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancellableChain.swift; sourceTree = ""; }; A91614D02B108D1B00F416EB /* TransportLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportLayer.swift; sourceTree = ""; }; A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelControlViewModel.swift; sourceTree = ""; }; A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = ""; }; @@ -2868,6 +2908,33 @@ path = RelayFilter; sourceTree = ""; }; + 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 = ""; + }; 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 */,