summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift
blob: b27d03f79e706ba60f3b57a3bc9c654bb527da1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//
//  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: @Sendable (Socks5ConnectReply) -> Void

    /// Failure handler invoked on error.
    let onFailure: @Sendable (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)
    }
}