From 026bfebedf88b14a22d21e32062bda9e35bd4891 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 --- ios/MullvadREST/RESTTransportStrategy.swift | 2 + .../AnyIPEndpoint+Socks5.swift | 33 ++ .../DeferredCancellable.swift | 43 +++ .../Socks5/NWConnection+Extensions.swift | 22 ++ .../Socks5/Socks5AddressType.swift | 15 + .../Socks5/Socks5Authentication.swift | 14 + .../Socks5/Socks5Command.swift | 15 + .../Socks5/Socks5ConnectCommand.swift | 46 +++ .../Socks5/Socks5ConnectNegotiation.swift | 115 +++++++ .../Socks5/Socks5Connection.swift | 221 ++++++++++++++ .../Socks5/Socks5Constants.swift | 13 + .../Socks5/Socks5DataStreamHandler.swift | 80 +++++ .../Socks5/Socks5Endpoint.swift | 111 +++++++ .../Socks5/Socks5EndpointReader.swift | 124 ++++++++ ios/MullvadTransport/Socks5/Socks5Error.swift | 54 ++++ .../Socks5/Socks5ForwardingProxy.swift | 281 ++++++++++++++++++ .../Socks5/Socks5Handshake.swift | 43 +++ .../Socks5/Socks5HandshakeNegotiation.swift | 68 +++++ .../Socks5/Socks5StatusCode.swift | 21 ++ .../Socks5Configuration.swift | 21 ++ ios/MullvadTransport/TransportProvider.swift | 13 + .../URLSessionSocks5Transport.swift | 113 +++++++ .../URLSessionTransport.swift | 2 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 96 +++++- 24 files changed, 1561 insertions(+), 5 deletions(-) create mode 100644 ios/MullvadTransport/AnyIPEndpoint+Socks5.swift create mode 100644 ios/MullvadTransport/DeferredCancellable.swift create mode 100644 ios/MullvadTransport/Socks5/NWConnection+Extensions.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5AddressType.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Authentication.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Command.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5ConnectCommand.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5ConnectNegotiation.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Connection.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Constants.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5DataStreamHandler.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Endpoint.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5EndpointReader.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Error.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5ForwardingProxy.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5Handshake.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5HandshakeNegotiation.swift create mode 100644 ios/MullvadTransport/Socks5/Socks5StatusCode.swift create mode 100644 ios/MullvadTransport/Socks5Configuration.swift create mode 100644 ios/MullvadTransport/URLSessionSocks5Transport.swift diff --git a/ios/MullvadREST/RESTTransportStrategy.swift b/ios/MullvadREST/RESTTransportStrategy.swift index d63b8c8833a6..084a66329b7e 100644 --- a/ios/MullvadREST/RESTTransportStrategy.swift +++ b/ios/MullvadREST/RESTTransportStrategy.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/MullvadTransport/AnyIPEndpoint+Socks5.swift b/ios/MullvadTransport/AnyIPEndpoint+Socks5.swift new file mode 100644 index 000000000000..986f8276fab3 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/DeferredCancellable.swift b/ios/MullvadTransport/DeferredCancellable.swift new file mode 100644 index 000000000000..d73104773d2b --- /dev/null +++ b/ios/MullvadTransport/DeferredCancellable.swift @@ -0,0 +1,43 @@ +// +// DeferredCancellable.swift +// MullvadTransport +// +// Created by pronebird on 23/10/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +/// Cancellable object that defers cancellation until the other token is connected to it. +final class DeferredCancellable: Cancellable { + private let stateLock = NSLock() + private var isCancelled = false + private var connectedTokens: [Cancellable] = [] + + init() {} + + /// Connect deferred cancellation token with some other. + /// + /// The token is cancelled immediately, if the deferred object is already cancelled. + func connect(_ token: Cancellable) { + stateLock.withLock { + if isCancelled { + token.cancel() + } else { + connectedTokens.append(token) + } + } + } + + /// Request cancellation. + /// + /// Cancels and releases any of the connected tokens. + func cancel() { + stateLock.withLock { + isCancelled = true + connectedTokens.forEach { $0.cancel() } + connectedTokens.removeAll() + } + } +} diff --git a/ios/MullvadTransport/Socks5/NWConnection+Extensions.swift b/ios/MullvadTransport/Socks5/NWConnection+Extensions.swift new file mode 100644 index 000000000000..d99f9e808185 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5AddressType.swift b/ios/MullvadTransport/Socks5/Socks5AddressType.swift new file mode 100644 index 000000000000..ad013d1ae966 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5Authentication.swift b/ios/MullvadTransport/Socks5/Socks5Authentication.swift new file mode 100644 index 000000000000..39a240d41ef7 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5Command.swift b/ios/MullvadTransport/Socks5/Socks5Command.swift new file mode 100644 index 000000000000..293acf218b10 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5ConnectCommand.swift b/ios/MullvadTransport/Socks5/Socks5ConnectCommand.swift new file mode 100644 index 000000000000..0ad4d23bbdaa --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5ConnectNegotiation.swift b/ios/MullvadTransport/Socks5/Socks5ConnectNegotiation.swift new file mode 100644 index 000000000000..3591bee748fc --- /dev/null +++ b/ios/MullvadTransport/Socks5/Socks5ConnectNegotiation.swift @@ -0,0 +1,115 @@ +// +// 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, contentContext, isComplete, 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 and evaluate the status code. 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. + let (statusCode, addressType) = try parsePartialReply(data: data) + + // Verify the status code. + guard case .succeeded = statusCode else { + throw Socks5Error.connectionRejected(statusCode) + } + + // Parse server bound endpoint when partial reply indicates success. + 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(), + let _ = iterator.next(), // 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/MullvadTransport/Socks5/Socks5Connection.swift b/ios/MullvadTransport/Socks5/Socks5Connection.swift new file mode 100644 index 000000000000..cc357ff968d0 --- /dev/null +++ b/ios/MullvadTransport/Socks5/Socks5Connection.swift @@ -0,0 +1,221 @@ +// +// Socks5Connection.swift +// MullvadTransport +// +// Created by pronebird on 19/10/2023. +// + +import Foundation +import Network + +/// A bidirection 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 + + /** + Initilizes 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 conncetion 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] _ in stream() }, + 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/MullvadTransport/Socks5/Socks5Constants.swift b/ios/MullvadTransport/Socks5/Socks5Constants.swift new file mode 100644 index 000000000000..325c7527da77 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5DataStreamHandler.swift b/ios/MullvadTransport/Socks5/Socks5DataStreamHandler.swift new file mode 100644 index 000000000000..b687892b5752 --- /dev/null +++ b/ios/MullvadTransport/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, contentContext, 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, contentContext, 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/MullvadTransport/Socks5/Socks5Endpoint.swift b/ios/MullvadTransport/Socks5/Socks5Endpoint.swift new file mode 100644 index 000000000000..1587991bf7ce --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5EndpointReader.swift b/ios/MullvadTransport/Socks5/Socks5EndpointReader.swift new file mode 100644 index 000000000000..35aee52533cf --- /dev/null +++ b/ios/MullvadTransport/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, contentContext, isComplete, 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, contentContext, isComplete, 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/MullvadTransport/Socks5/Socks5Error.swift b/ios/MullvadTransport/Socks5/Socks5Error.swift new file mode 100644 index 000000000000..992c402160f9 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5ForwardingProxy.swift b/ios/MullvadTransport/Socks5/Socks5ForwardingProxy.swift new file mode 100644 index 000000000000..0b26bc3b92ec --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5Handshake.swift b/ios/MullvadTransport/Socks5/Socks5Handshake.swift new file mode 100644 index 000000000000..23fadc64fd45 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5/Socks5HandshakeNegotiation.swift b/ios/MullvadTransport/Socks5/Socks5HandshakeNegotiation.swift new file mode 100644 index 000000000000..f3336c9b6461 --- /dev/null +++ b/ios/MullvadTransport/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, context, isComplete, 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/MullvadTransport/Socks5/Socks5StatusCode.swift b/ios/MullvadTransport/Socks5/Socks5StatusCode.swift new file mode 100644 index 000000000000..9832c156a13a --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/Socks5Configuration.swift b/ios/MullvadTransport/Socks5Configuration.swift new file mode 100644 index 000000000000..39033821d459 --- /dev/null +++ b/ios/MullvadTransport/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/MullvadTransport/TransportProvider.swift b/ios/MullvadTransport/TransportProvider.swift index 88c3e1fc8a23..5de3df9545aa 100644 --- a/ios/MullvadTransport/TransportProvider.swift +++ b/ios/MullvadTransport/TransportProvider.swift @@ -82,6 +82,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. @@ -151,6 +162,8 @@ public final class TransportProvider: RESTTransportProvider { currentTransport = shadowsocks() case .useURLSession: currentTransport = urlSessionTransport + case .useSocks5: + currentTransport = socks5() } } return currentTransport diff --git a/ios/MullvadTransport/URLSessionSocks5Transport.swift b/ios/MullvadTransport/URLSessionSocks5Transport.swift new file mode 100644 index 000000000000..371562e0c8da --- /dev/null +++ b/ios/MullvadTransport/URLSessionSocks5Transport.swift @@ -0,0 +1,113 @@ +// +// URLSessionSocks5Transport.swift +// MullvadTransport +// +// Created by pronebird on 23/10/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import MullvadREST +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 deferred = DeferredCancellable() + + 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 deferred to the data task cancellation token. + deferred.connect(token) + } else { + completion(nil, nil, URLError(.cancelled)) + } + } + + return deferred + } + + /// 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/MullvadTransport/URLSessionTransport.swift b/ios/MullvadTransport/URLSessionTransport.swift index 00618230855f..21af6e199c0b 100644 --- a/ios/MullvadTransport/URLSessionTransport.swift +++ b/ios/MullvadTransport/URLSessionTransport.swift @@ -1,6 +1,6 @@ // // URLSessionTransport.swift -// MullvadREST +// MullvadTransport // // Created by Sajad Vishkai on 2022-10-03. // Copyright © 2022 Mullvad VPN AB. All rights reserved. diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8a12966bce28..0f9c65ee5436 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -116,6 +116,26 @@ 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; }; 585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; }; 585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; }; + 585F5A672AE68688001D9DF7 /* Socks5StatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A572AE6867F001D9DF7 /* Socks5StatusCode.swift */; }; + 585F5A682AE68688001D9DF7 /* Socks5ForwardingProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A582AE68680001D9DF7 /* Socks5ForwardingProxy.swift */; }; + 585F5A692AE68688001D9DF7 /* Socks5ConnectCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A592AE68680001D9DF7 /* Socks5ConnectCommand.swift */; }; + 585F5A6A2AE68688001D9DF7 /* Socks5EndpointReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A5A2AE68681001D9DF7 /* Socks5EndpointReader.swift */; }; + 585F5A6B2AE68688001D9DF7 /* Socks5Handshake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A5B2AE68681001D9DF7 /* Socks5Handshake.swift */; }; + 585F5A6C2AE68688001D9DF7 /* Socks5Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A5C2AE68682001D9DF7 /* Socks5Command.swift */; }; + 585F5A6D2AE68688001D9DF7 /* Socks5HandshakeNegotiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A5D2AE68683001D9DF7 /* Socks5HandshakeNegotiation.swift */; }; + 585F5A6E2AE68688001D9DF7 /* Socks5DataStreamHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A5E2AE68683001D9DF7 /* Socks5DataStreamHandler.swift */; }; + 585F5A6F2AE68688001D9DF7 /* Socks5Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A5F2AE68684001D9DF7 /* Socks5Error.swift */; }; + 585F5A702AE68688001D9DF7 /* Socks5Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A602AE68684001D9DF7 /* Socks5Connection.swift */; }; + 585F5A712AE68688001D9DF7 /* Socks5AddressType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A612AE68685001D9DF7 /* Socks5AddressType.swift */; }; + 585F5A722AE68688001D9DF7 /* Socks5Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A622AE68686001D9DF7 /* Socks5Constants.swift */; }; + 585F5A732AE68688001D9DF7 /* Socks5Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A632AE68686001D9DF7 /* Socks5Endpoint.swift */; }; + 585F5A742AE68688001D9DF7 /* Socks5Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A642AE68687001D9DF7 /* Socks5Authentication.swift */; }; + 585F5A752AE68688001D9DF7 /* Socks5ConnectNegotiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A652AE68687001D9DF7 /* Socks5ConnectNegotiation.swift */; }; + 585F5A762AE68688001D9DF7 /* NWConnection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A662AE68688001D9DF7 /* NWConnection+Extensions.swift */; }; + 585F5A782AE6879A001D9DF7 /* URLSessionSocks5Transport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A772AE6879A001D9DF7 /* URLSessionSocks5Transport.swift */; }; + 585F5A7A2AE6A523001D9DF7 /* DeferredCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A792AE6A523001D9DF7 /* DeferredCancellable.swift */; }; + 585F5A7C2AE6A537001D9DF7 /* AnyIPEndpoint+Socks5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A7B2AE6A537001D9DF7 /* AnyIPEndpoint+Socks5.swift */; }; + 585F5A7E2AE6B62D001D9DF7 /* Socks5Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585F5A7D2AE6B62D001D9DF7 /* Socks5Configuration.swift */; }; 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */; }; 586168692976F6BD00EF8598 /* DisplayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586168682976F6BD00EF8598 /* DisplayError.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; @@ -1267,6 +1287,26 @@ 585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = ""; }; 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = ""; }; 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStoreReceiptOperation.swift; sourceTree = ""; }; + 585F5A572AE6867F001D9DF7 /* Socks5StatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5StatusCode.swift; sourceTree = ""; }; + 585F5A582AE68680001D9DF7 /* Socks5ForwardingProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ForwardingProxy.swift; sourceTree = ""; }; + 585F5A592AE68680001D9DF7 /* Socks5ConnectCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectCommand.swift; sourceTree = ""; }; + 585F5A5A2AE68681001D9DF7 /* Socks5EndpointReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5EndpointReader.swift; sourceTree = ""; }; + 585F5A5B2AE68681001D9DF7 /* Socks5Handshake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Handshake.swift; sourceTree = ""; }; + 585F5A5C2AE68682001D9DF7 /* Socks5Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Command.swift; sourceTree = ""; }; + 585F5A5D2AE68683001D9DF7 /* Socks5HandshakeNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5HandshakeNegotiation.swift; sourceTree = ""; }; + 585F5A5E2AE68683001D9DF7 /* Socks5DataStreamHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5DataStreamHandler.swift; sourceTree = ""; }; + 585F5A5F2AE68684001D9DF7 /* Socks5Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Error.swift; sourceTree = ""; }; + 585F5A602AE68684001D9DF7 /* Socks5Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Connection.swift; sourceTree = ""; }; + 585F5A612AE68685001D9DF7 /* Socks5AddressType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5AddressType.swift; sourceTree = ""; }; + 585F5A622AE68686001D9DF7 /* Socks5Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Constants.swift; sourceTree = ""; }; + 585F5A632AE68686001D9DF7 /* Socks5Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Endpoint.swift; sourceTree = ""; }; + 585F5A642AE68687001D9DF7 /* Socks5Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Authentication.swift; sourceTree = ""; }; + 585F5A652AE68687001D9DF7 /* Socks5ConnectNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectNegotiation.swift; sourceTree = ""; }; + 585F5A662AE68688001D9DF7 /* NWConnection+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWConnection+Extensions.swift"; sourceTree = ""; }; + 585F5A772AE6879A001D9DF7 /* URLSessionSocks5Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSocks5Transport.swift; sourceTree = ""; }; + 585F5A792AE6A523001D9DF7 /* DeferredCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredCancellable.swift; sourceTree = ""; }; + 585F5A7B2AE6A537001D9DF7 /* AnyIPEndpoint+Socks5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyIPEndpoint+Socks5.swift"; sourceTree = ""; }; + 585F5A7D2AE6B62D001D9DF7 /* Socks5Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socks5Configuration.swift; sourceTree = ""; }; 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryInAppNotificationProvider.swift; sourceTree = ""; }; 586168682976F6BD00EF8598 /* DisplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayError.swift; sourceTree = ""; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = ""; }; @@ -2309,6 +2349,29 @@ path = RelayCacheTracker; sourceTree = ""; }; + 585F5A562AE6866F001D9DF7 /* Socks5 */ = { + isa = PBXGroup; + children = ( + 585F5A662AE68688001D9DF7 /* NWConnection+Extensions.swift */, + 585F5A612AE68685001D9DF7 /* Socks5AddressType.swift */, + 585F5A642AE68687001D9DF7 /* Socks5Authentication.swift */, + 585F5A5C2AE68682001D9DF7 /* Socks5Command.swift */, + 585F5A592AE68680001D9DF7 /* Socks5ConnectCommand.swift */, + 585F5A602AE68684001D9DF7 /* Socks5Connection.swift */, + 585F5A652AE68687001D9DF7 /* Socks5ConnectNegotiation.swift */, + 585F5A622AE68686001D9DF7 /* Socks5Constants.swift */, + 585F5A5E2AE68683001D9DF7 /* Socks5DataStreamHandler.swift */, + 585F5A632AE68686001D9DF7 /* Socks5Endpoint.swift */, + 585F5A5A2AE68681001D9DF7 /* Socks5EndpointReader.swift */, + 585F5A5F2AE68684001D9DF7 /* Socks5Error.swift */, + 585F5A582AE68680001D9DF7 /* Socks5ForwardingProxy.swift */, + 585F5A5B2AE68681001D9DF7 /* Socks5Handshake.swift */, + 585F5A5D2AE68683001D9DF7 /* Socks5HandshakeNegotiation.swift */, + 585F5A572AE6867F001D9DF7 /* Socks5StatusCode.swift */, + ); + path = Socks5; + sourceTree = ""; + }; 5864859729A0D012006C5743 /* Presentation controllers */ = { isa = PBXGroup; children = ( @@ -2965,14 +3028,19 @@ A97F1F422A1F4E1A00ECEFDE /* MullvadTransport */ = { isa = PBXGroup; children = ( + 585F5A7B2AE6A537001D9DF7 /* AnyIPEndpoint+Socks5.swift */, + 585F5A792AE6A523001D9DF7 /* DeferredCancellable.swift */, A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */, 586F2BE129F6916F009E6924 /* shadowsocks.h */, - 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */, - 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */, - 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */, - A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */, A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */, A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */, + 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */, + 585F5A562AE6866F001D9DF7 /* Socks5 */, + A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */, + 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */, + 585F5A772AE6879A001D9DF7 /* URLSessionSocks5Transport.swift */, + 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */, + 585F5A7D2AE6B62D001D9DF7 /* Socks5Configuration.swift */, ); path = MullvadTransport; sourceTree = ""; @@ -4642,11 +4710,31 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 585F5A7E2AE6B62D001D9DF7 /* Socks5Configuration.swift in Sources */, + 585F5A732AE68688001D9DF7 /* Socks5Endpoint.swift in Sources */, + 585F5A6D2AE68688001D9DF7 /* Socks5HandshakeNegotiation.swift in Sources */, + 585F5A762AE68688001D9DF7 /* NWConnection+Extensions.swift in Sources */, + 585F5A722AE68688001D9DF7 /* Socks5Constants.swift in Sources */, 5822C0052A3724A800A3A5FB /* ShadowsocksConfiguration.swift in Sources */, + 585F5A752AE68688001D9DF7 /* Socks5ConnectNegotiation.swift in Sources */, + 585F5A672AE68688001D9DF7 /* Socks5StatusCode.swift in Sources */, A95F86B82A1F547000245DAC /* ShadowsocksProxy.swift in Sources */, + 585F5A782AE6879A001D9DF7 /* URLSessionSocks5Transport.swift in Sources */, + 585F5A6B2AE68688001D9DF7 /* Socks5Handshake.swift in Sources */, + 585F5A682AE68688001D9DF7 /* Socks5ForwardingProxy.swift in Sources */, + 585F5A7A2AE6A523001D9DF7 /* DeferredCancellable.swift in Sources */, + 585F5A692AE68688001D9DF7 /* Socks5ConnectCommand.swift in Sources */, A95F86B72A1F53BA00245DAC /* URLSessionTransport.swift in Sources */, 5822C0042A3724A800A3A5FB /* ShadowsocksConfigurationCache.swift in Sources */, + 585F5A712AE68688001D9DF7 /* Socks5AddressType.swift in Sources */, + 585F5A7C2AE6A537001D9DF7 /* AnyIPEndpoint+Socks5.swift in Sources */, + 585F5A702AE68688001D9DF7 /* Socks5Connection.swift in Sources */, + 585F5A6A2AE68688001D9DF7 /* Socks5EndpointReader.swift in Sources */, + 585F5A6F2AE68688001D9DF7 /* Socks5Error.swift in Sources */, + 585F5A6C2AE68688001D9DF7 /* Socks5Command.swift in Sources */, A9D99BA02A1F7F3A00DE27D3 /* TransportProvider.swift in Sources */, + 585F5A742AE68688001D9DF7 /* Socks5Authentication.swift in Sources */, + 585F5A6E2AE68688001D9DF7 /* Socks5DataStreamHandler.swift in Sources */, 58E0E2842A3718CE002E3420 /* URLSessionShadowsocksTransport.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;