Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add socks5 proxy [POC] IOS-358 #5348

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ios/MullvadREST/Transport/Direct/URLSessionTransport.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// URLSessionTransport.swift
// MullvadREST
// MullvadTransport
//
// Created by Mojgan on 2023-12-08.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
Expand Down
33 changes: 33 additions & 0 deletions ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
43 changes: 43 additions & 0 deletions ios/MullvadREST/Transport/Socks5/CancellableChain.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
22 changes: 22 additions & 0 deletions ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
15 changes: 15 additions & 0 deletions ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions ios/MullvadREST/Transport/Socks5/Socks5Command.swift
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
46 changes: 46 additions & 0 deletions ios/MullvadREST/Transport/Socks5/Socks5ConnectCommand.swift
Original file line number Diff line number Diff line change
@@ -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
}
109 changes: 109 additions & 0 deletions ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading