Skip to content

Commit ae338c5

Browse files
committed
Add SOCKS support to native-connect hook
1 parent bcbe288 commit ae338c5

File tree

2 files changed

+164
-52
lines changed

2 files changed

+164
-52
lines changed

config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ const IGNORED_NON_HTTP_PORTS = [];
3838
// to the same proxy port & address as TCP connections.
3939
const BLOCK_HTTP3 = true;
4040

41+
// Set this to true if your proxy supports SOCKS5 connections.
42+
// This makes it possible for native-connect-hook to redirect
43+
// non-HTTP traffic through your proxy (to view it raw, and
44+
// avoid breaking non-HTTP traffic en route).
45+
const PROXY_SUPPORTS_SOCKS5 = false;
46+
47+
4148
// ----------------------------------------------------------------------------
4249
// You don't need to modify any of the below, it just checks and applies some
4350
// of the configuration that you've entered above.

native-connect-hook.js

Lines changed: 157 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,36 @@
1717
* SPDX-FileCopyrightText: Tim Perry <[email protected]>
1818
*/
1919

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'), {
3850
onEnter(args) {
3951
const fd = this.sockFd = args[0].toInt32();
4052
const sockType = Socket.type(fd);
@@ -81,6 +93,18 @@ if (!connectFn) { // Should always be set, but just in case
8193
this.state = 'Blocked';
8294
} else if (!shouldBeIgnored) {
8395
// 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+
}
84108

85109
console.log(`Manually intercepting connection to ${getReadableAddress(hostBytes, isIPv6)}:${port}`);
86110

@@ -96,7 +120,6 @@ if (!connectFn) { // Should always be set, but just in case
96120
// Skip 4 bytes: 2 family, 2 port
97121
addrPtr.add(4).writeByteArray(PROXY_HOST_IPv4_BYTES);
98122
}
99-
this.state = 'Intercepted';
100123
} else {
101124
// Explicitly being left alone
102125
if (DEBUG_MODE) {
@@ -111,15 +134,43 @@ if (!connectFn) { // Should always be set, but just in case
111134

112135
// N.b. we ignore all non-TCP connections: both UDP and Unix streams
113136
},
114-
onLeave: function (result) {
137+
onLeave: function (retval) {
115138
if (!DEBUG_MODE || this.state === 'ignored') return;
116139

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+
}
123174
}
124175
});
125176

@@ -128,33 +179,87 @@ if (!connectFn) { // Should always be set, but just in case
128179
? 'all'
129180
: 'all unrecognized'
130181
} 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-
}
141182

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+
}
149191

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+
}
155228

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
160230

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

Comments
 (0)