-
Notifications
You must be signed in to change notification settings - Fork 354
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Andrej Mihajlov
committed
Oct 23, 2023
1 parent
90a275f
commit bd6e0e4
Showing
24 changed files
with
1,561 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
115
ios/MullvadTransport/Socks5/Socks5ConnectNegotiation.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.