17
17
* SPDX-FileCopyrightText: Tim Perry <[email protected] >
18
18
*/
19
19
20
- const PROXY_HOST_IPv4_BYTES = PROXY_HOST . split ( '.' ) . map ( part => parseInt ( part , 10 ) ) ;
21
- const IPv6_MAPPING_PREFIX_BYTES = [ 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0xff , 0xff ] ;
22
- const PROXY_HOST_IPv6_BYTES = IPv6_MAPPING_PREFIX_BYTES . concat ( PROXY_HOST_IPv4_BYTES ) ;
23
-
24
- let connectFn = null ;
25
- try {
26
- connectFn =
27
- Process . findModuleByName ( 'libc.so' ) ?. findExportByName ( 'connect' ) ?? // Android
28
- Process . findModuleByName ( 'libc.so.6' ) ?. findExportByName ( 'connect' ) ?? // Linux
29
- Process . findModuleByName ( 'libsystem_kernel.dylib' ) ?. findExportByName ( 'connect' ) ; // iOS
30
- } catch ( e ) {
31
- console . error ( "Failed to find 'connect' export:" , e ) ;
32
- }
33
-
34
- if ( ! connectFn ) { // Should always be set, but just in case
35
- console . warn ( 'Could not find libc connect() function to hook raw traffic' ) ;
36
- } else {
37
- Interceptor . attach ( connectFn , {
20
+ ( ( ) => {
21
+ const PROXY_HOST_IPv4_BYTES = PROXY_HOST . split ( '.' ) . map ( part => parseInt ( part , 10 ) ) ;
22
+ const IPv6_MAPPING_PREFIX_BYTES = [ 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0x0 , 0xff , 0xff ] ;
23
+ const PROXY_HOST_IPv6_BYTES = IPv6_MAPPING_PREFIX_BYTES . concat ( PROXY_HOST_IPv4_BYTES ) ;
24
+
25
+ // Flags for fcntl():
26
+ const F_GETFL = 3 ;
27
+ const F_SETFL = 4 ;
28
+ const O_NONBLOCK = ( Process . platform === 'darwin' )
29
+ ? 4
30
+ : 2048 ; // Linux/Android
31
+
32
+ let fcntl , send , recv ;
33
+ try {
34
+ const systemModule = Process . findModuleByName ( 'libc.so' ) ?? // Android
35
+ Process . findModuleByName ( 'libc.so.6' ) ?? // Linux
36
+ Process . findModuleByName ( 'libsystem_kernel.dylib' ) ; // iOS
37
+
38
+ if ( ! systemModule ) throw new Error ( "Could not find libc or libsystem_kernel" ) ;
39
+
40
+ fcntl = new NativeFunction ( systemModule . getExportByName ( 'fcntl' ) , 'int' , [ 'int' , 'int' , 'int' ] ) ;
41
+ send = new NativeFunction ( systemModule . getExportByName ( 'send' ) , 'ssize_t' , [ 'int' , 'pointer' , 'size_t' , 'int' ] ) ;
42
+ recv = new NativeFunction ( systemModule . getExportByName ( 'recv' ) , 'ssize_t' , [ 'int' , 'pointer' , 'size_t' , 'int' ] ) ;
43
+ } catch ( e ) {
44
+ console . error ( "Failed to set up native hooks:" , e . message ) ;
45
+ console . warn ( 'Could not initialize system functions to to hook raw traffic' ) ;
46
+ return ;
47
+ }
48
+
49
+ Interceptor . attach ( systemModule . getExportByName ( 'connect' ) , {
38
50
onEnter ( args ) {
39
51
const fd = this . sockFd = args [ 0 ] . toInt32 ( ) ;
40
52
const sockType = Socket . type ( fd ) ;
@@ -81,6 +93,18 @@ if (!connectFn) { // Should always be set, but just in case
81
93
this . state = 'Blocked' ;
82
94
} else if ( ! shouldBeIgnored ) {
83
95
// Otherwise, it's an unintercepted connection that should be captured:
96
+ this . state = 'intercepting' ;
97
+
98
+ // For SOCKS, we preserve the original destionation to use in the SOCKS handshake later
99
+ // and we temporarily set the socket to blocking mode to do the handshake itself.
100
+ if ( PROXY_SUPPORTS_SOCKS5 ) {
101
+ this . originalDestination = { host : hostBytes , port, isIPv6 } ;
102
+ this . originalFlags = fcntl ( this . sockFd , F_GETFL , 0 ) ;
103
+ this . isNonBlocking = ( this . originalFlags & O_NONBLOCK ) !== 0 ;
104
+ if ( this . isNonBlocking ) {
105
+ fcntl ( this . sockFd , F_SETFL , this . originalFlags & ~ O_NONBLOCK ) ;
106
+ }
107
+ }
84
108
85
109
console . log ( `Manually intercepting connection to ${ getReadableAddress ( hostBytes , isIPv6 ) } :${ port } ` ) ;
86
110
@@ -96,7 +120,6 @@ if (!connectFn) { // Should always be set, but just in case
96
120
// Skip 4 bytes: 2 family, 2 port
97
121
addrPtr . add ( 4 ) . writeByteArray ( PROXY_HOST_IPv4_BYTES ) ;
98
122
}
99
- this . state = 'Intercepted' ;
100
123
} else {
101
124
// Explicitly being left alone
102
125
if ( DEBUG_MODE ) {
@@ -111,15 +134,43 @@ if (!connectFn) { // Should always be set, but just in case
111
134
112
135
// N.b. we ignore all non-TCP connections: both UDP and Unix streams
113
136
} ,
114
- onLeave : function ( result ) {
137
+ onLeave : function ( retval ) {
115
138
if ( ! DEBUG_MODE || this . state === 'ignored' ) return ;
116
139
117
- const fd = this . sockFd ;
118
- const sockType = Socket . type ( fd ) ;
119
- const address = Socket . peerAddress ( fd ) ;
120
- console . debug (
121
- `${ this . state } ${ sockType } fd ${ fd } to ${ JSON . stringify ( address ) } (${ result . toInt32 ( ) } )`
122
- ) ;
140
+ if ( this . state === 'intercepting' ) {
141
+ const connectSuccess = retval . toInt32 ( ) === 0 ;
142
+
143
+ if ( PROXY_SUPPORTS_SOCKS5 ) {
144
+ let handshakeSuccess = false ;
145
+
146
+ const { host, port, isIPv6 } = this . originalDestination ;
147
+ if ( connectSuccess ) {
148
+ handshakeSuccess = performSocksHandshake ( this . sockFd , host , port , isIPv6 ) ;
149
+ } else {
150
+ console . error ( `SOCKS: Failed to connect to proxy at ${ PROXY_HOST } :${ PROXY_PORT } ` ) ;
151
+ }
152
+
153
+ if ( this . isNonBlocking ) {
154
+ fcntl ( this . sockFd , F_SETFL , this . originalFlags ) ;
155
+ }
156
+
157
+ if ( handshakeSuccess ) {
158
+ const readableHost = getReadableAddress ( host , isIPv6 ) ;
159
+ if ( DEBUG_MODE ) console . debug ( `SOCKS redirect successful for fd ${ this . sockFd } to ${ readableHost } :${ port } ` ) ;
160
+ retval . replace ( 0 ) ;
161
+ } else {
162
+ if ( DEBUG_MODE ) console . error ( `SOCKS redirect FAILED for fd ${ this . sockFd } ` ) ;
163
+ retval . replace ( - 1 ) ;
164
+ }
165
+ }
166
+ } else {
167
+ const fd = this . sockFd ;
168
+ const sockType = Socket . type ( fd ) ;
169
+ const address = Socket . peerAddress ( fd ) ;
170
+ console . debug (
171
+ `${ this . state } ${ sockType } fd ${ fd } to ${ JSON . stringify ( address ) } (${ retval . toInt32 ( ) } )`
172
+ ) ;
173
+ }
123
174
}
124
175
} ) ;
125
176
@@ -128,33 +179,87 @@ if (!connectFn) { // Should always be set, but just in case
128
179
? 'all'
129
180
: 'all unrecognized'
130
181
} TCP connections to ${ PROXY_HOST } :${ PROXY_PORT } ==`) ;
131
- }
132
-
133
- const getReadableAddress = (
134
- /** @type {Uint8Array } */ hostBytes ,
135
- /** @type {boolean } */ isIPv6
136
- ) => {
137
- if ( ! isIPv6 ) {
138
- // Return simple a.b.c.d IPv4 format:
139
- return [ ...hostBytes ] . map ( x => x . toString ( ) ) . join ( '.' ) ;
140
- }
141
182
142
- if (
143
- hostBytes . slice ( 0 , 10 ) . every ( b => b === 0 ) &&
144
- hostBytes . slice ( 10 , 12 ) . every ( b => b === 255 )
145
- ) {
146
- // IPv4-mapped IPv6 address - print as IPv4 for readability
147
- return '::ffff:' + [ ...hostBytes . slice ( 12 ) ] . map ( x => x . toString ( ) ) . join ( '.' ) ;
148
- }
183
+ const getReadableAddress = (
184
+ /** @type {Uint8Array } */ hostBytes ,
185
+ /** @type {boolean } */ isIPv6
186
+ ) => {
187
+ if ( ! isIPv6 ) {
188
+ // Return simple a.b.c.d IPv4 format:
189
+ return [ ...hostBytes ] . map ( x => x . toString ( ) ) . join ( '.' ) ;
190
+ }
149
191
150
- else {
151
- // Real IPv6:
152
- return `[${ [ ...hostBytes ] . map ( x => x . toString ( 16 ) ) . join ( ':' ) } ]` ;
153
- }
154
- } ;
192
+ if (
193
+ hostBytes . slice ( 0 , 10 ) . every ( b => b === 0 ) &&
194
+ hostBytes . slice ( 10 , 12 ) . every ( b => b === 255 )
195
+ ) {
196
+ // IPv4-mapped IPv6 address - print as IPv4 for readability
197
+ return '::ffff:' + [ ...hostBytes . slice ( 12 ) ] . map ( x => x . toString ( ) ) . join ( '.' ) ;
198
+ }
199
+
200
+ else {
201
+ // Real IPv6:
202
+ return `[${ [ ...hostBytes ] . map ( x => x . toString ( 16 ) ) . join ( ':' ) } ]` ;
203
+ }
204
+ } ;
205
+
206
+ const areArraysEqual = ( arrayA , arrayB ) => {
207
+ if ( arrayA . length !== arrayB . length ) return false ;
208
+ return arrayA . every ( ( x , i ) => arrayB [ i ] === x ) ;
209
+ } ;
210
+
211
+ function performSocksHandshake ( sockfd , targetHostBytes , targetPort , isIPv6 ) {
212
+ const hello = Memory . alloc ( 3 ) . writeByteArray ( [ 0x05 , 0x01 , 0x00 ] ) ;
213
+ if ( send ( sockfd , hello , 3 , 0 ) < 0 ) {
214
+ console . error ( "SOCKS: Failed to send hello" ) ;
215
+ return false ;
216
+ }
217
+
218
+ const response = Memory . alloc ( 2 ) ;
219
+ if ( recv ( sockfd , response , 2 , 0 ) < 0 ) {
220
+ console . error ( "SOCKS: Failed to receive server choice" ) ;
221
+ return false ;
222
+ }
223
+
224
+ if ( response . readU8 ( ) !== 0x05 || response . add ( 1 ) . readU8 ( ) !== 0x00 ) {
225
+ console . error ( "SOCKS: Server rejected auth method" ) ;
226
+ return false ;
227
+ }
155
228
156
- const areArraysEqual = ( arrayA , arrayB ) => {
157
- if ( arrayA . length !== arrayB . length ) return false ;
158
- return arrayA . every ( ( x , i ) => arrayB [ i ] === x ) ;
159
- } ;
229
+ let req = [ 0x05 , 0x01 , 0x00 ] ; // VER, CMD(CONNECT), RSV
160
230
231
+ if ( isIPv6 ) {
232
+ req . push ( 0x04 ) ; // ATYP: IPv6
233
+ } else { // IPv4
234
+ req . push ( 0x01 ) ; // ATYP: IPv4
235
+ }
236
+
237
+ req . push ( ...targetHostBytes , ( targetPort >> 8 ) & 0xff , targetPort & 0xff ) ;
238
+ const reqBuf = Memory . alloc ( req . length ) . writeByteArray ( req ) ;
239
+
240
+ if ( send ( sockfd , reqBuf , req . length , 0 ) < 0 ) {
241
+ console . error ( "SOCKS: Failed to send connection request" ) ;
242
+ return false ;
243
+ }
244
+
245
+ const replyHeader = Memory . alloc ( 4 ) ;
246
+ if ( recv ( sockfd , replyHeader , 4 , 0 ) < 0 ) {
247
+ console . error ( "SOCKS: Failed to receive reply header" ) ;
248
+ return false ;
249
+ }
250
+
251
+ const replyCode = replyHeader . add ( 1 ) . readU8 ( ) ;
252
+ if ( replyCode !== 0x00 ) {
253
+ console . error ( `SOCKS: Server returned error code ${ replyCode } ` ) ;
254
+ return false ;
255
+ }
256
+
257
+ const atyp = replyHeader . add ( 3 ) . readU8 ( ) ;
258
+ let remainingBytes = 0 ;
259
+ if ( atyp === 0x01 ) remainingBytes = 4 + 2 ; // IPv4 + port
260
+ else if ( atyp === 0x04 ) remainingBytes = 16 + 2 ; // IPv6 + port
261
+ if ( remainingBytes > 0 ) recv ( sockfd , Memory . alloc ( remainingBytes ) , remainingBytes , 0 ) ;
262
+
263
+ return true ;
264
+ }
265
+ } ) ( ) ;
0 commit comments