Skip to content

Commit

Permalink
Add socks5 forwarding proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrej Mihajlov committed Oct 30, 2023
1 parent ede2daa commit a9fcbfe
Show file tree
Hide file tree
Showing 24 changed files with 1,561 additions and 5 deletions.
2 changes: 2 additions & 0 deletions ios/MullvadREST/RESTTransportStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions ios/MullvadTransport/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/MullvadTransport/DeferredCancellable.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
22 changes: 22 additions & 0 deletions ios/MullvadTransport/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/MullvadTransport/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/MullvadTransport/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/MullvadTransport/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
}
46 changes: 46 additions & 0 deletions ios/MullvadTransport/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
}
115 changes: 115 additions & 0 deletions ios/MullvadTransport/Socks5/Socks5ConnectNegotiation.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit a9fcbfe

Please sign in to comment.