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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
|
//
// Socks5Connection.swift
// MullvadTransport
//
// Created by pronebird on 19/10/2023.
//
import Foundation
import Network
/// A bidirectional data connection between a local endpoint and remote endpoint over socks proxy.
final class Socks5Connection: Sendable {
/// The remote endpoint to which the client wants to establish connection over the socks proxy.
let remoteServerEndpoint: Socks5Endpoint
let configuration: Socks5Configuration
/**
Initializes 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,
configuration: Socks5Configuration
) {
self.queue = queue
self.remoteServerEndpoint = remoteServerEndpoint
self.localConnection = localConnection
self.remoteConnection = NWConnection(to: socksProxyEndpoint, using: .tcp)
self.configuration = configuration
}
/**
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 connection to avoid missing updates to the connection state.
- Parameter newStateHandler: state handler block.
*/
func setStateHandler(_ newStateHandler: (@Sendable (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
nonisolated(unsafe) private var stateHandler: (@Sendable (Socks5Connection, State) -> Void)?
nonisolated(unsafe) 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() {
var handshake = Socks5Handshake()
if configuration.username != nil && configuration.password != nil {
handshake.methods.append(.usernamePassword)
}
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:
// Username + password authentication sends the data in plain text to the server
// And then continues like the `notRequired` case after the server has authenticated the client.
let authentication = Socks5Authentication(
connection: remoteConnection,
endpoint: remoteServerEndpoint,
configuration: configuration
)
authentication.authenticate(
onComplete: { [self] in
connect()
},
onFailure: { [self] error in
handleError(error)
})
}
}
/// 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] reply in
if case .succeeded = reply.status {
stream()
} else {
handleError(Socks5Error.connectionRejected(reply.status))
}
},
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()
}
}
|