@fanboynz/network-scanner 2.0.64 → 2.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Local no-auth SOCKS5 relay for authenticated SOCKS5 upstreams.
3
+ *
4
+ * Chromium cannot authenticate SOCKS5 proxies (crbug.com/256785 — it only
5
+ * implements the no-auth method 0x00; credentials in --proxy-server are
6
+ * discarded, and page.authenticate() is HTTP-407-only so it can't help —
7
+ * SOCKS auth happens at the TCP handshake before any HTTP).
8
+ *
9
+ * Workaround: run an in-process no-auth SOCKS5 server bound to 127.0.0.1.
10
+ * Chromium connects to it without auth (which it CAN do); for each
11
+ * connection we open an authenticated tunnel to the real upstream via the
12
+ * `socks` package (RFC 1929 user/pass) and pipe the two together. Domain
13
+ * address types are forwarded as hostnames so remote DNS still works
14
+ * end-to-end (no DNS leak).
15
+ *
16
+ * Relays are keyed by upstream identity and reused. closeAllRelays() must
17
+ * be called on scan exit / signal so listening sockets don't leak.
18
+ */
19
+
20
+ const net = require('net');
21
+ const { SocksClient } = require('socks');
22
+ const { formatLogMessage } = require('./colorize');
23
+
24
+ // upstreamKey -> { server, port, activeSockets:Set<net.Socket> }
25
+ const _relays = new Map();
26
+
27
+ function upstreamKey(u) {
28
+ return `${u.host}:${u.port}:${u.username || ''}`;
29
+ }
30
+
31
+ /**
32
+ * Handle one Chromium->relay connection: minimal SOCKS5 server handshake,
33
+ * then an authenticated upstream tunnel, then bidirectional pipe.
34
+ */
35
+ // Bail on a client that connects and never completes SOCKS5 negotiation.
36
+ // Generous enough for a Chromium loopback handshake (microseconds), short
37
+ // enough to catch a stalled / half-open client before the OS TCP keepalive
38
+ // notices (default ~2 hours on Linux).
39
+ const HANDSHAKE_TIMEOUT_MS = 10000;
40
+
41
+ function handleClient(client, upstream, forceDebug) {
42
+ let phase = 'greeting';
43
+ let buf = Buffer.alloc(0);
44
+ let upstreamSock = null;
45
+ let settled = false;
46
+ // Handshake-phase watchdog handle. Assigned after cleanup is declared so
47
+ // both references in this scope resolve unambiguously.
48
+ let handshakeTimer = null;
49
+
50
+ const cleanup = () => {
51
+ if (settled) return;
52
+ settled = true;
53
+ if (handshakeTimer) { clearTimeout(handshakeTimer); handshakeTimer = null; }
54
+ try { client.destroy(); } catch (_) {}
55
+ if (upstreamSock) { try { upstreamSock.destroy(); } catch (_) {} }
56
+ };
57
+
58
+ // Bail on a client that connects and never completes SOCKS5 negotiation
59
+ // (stalled / half-open / non-SOCKS protocol). Without this, such a socket
60
+ // sits in activeSockets until the OS TCP keepalive notices — default
61
+ // ~2 hours on Linux. unref'd so a pending watchdog never holds the
62
+ // process alive after closeAllRelays().
63
+ handshakeTimer = setTimeout(() => {
64
+ if (phase !== 'piping') {
65
+ if (forceDebug) {
66
+ console.log(formatLogMessage('proxy', `[socks-relay] handshake timeout (phase=${phase}) — closing`));
67
+ }
68
+ cleanup();
69
+ }
70
+ }, HANDSHAKE_TIMEOUT_MS);
71
+ if (typeof handshakeTimer.unref === 'function') handshakeTimer.unref();
72
+
73
+ const onData = async (chunk) => {
74
+ buf = Buffer.concat([buf, chunk]);
75
+ try {
76
+ if (phase === 'greeting') {
77
+ // [0x05, NMETHODS, METHODS...]
78
+ if (buf.length < 2) return;
79
+ const nMethods = buf[1];
80
+ if (buf.length < 2 + nMethods) return;
81
+ const offered = buf.subarray(2, 2 + nMethods);
82
+ buf = buf.subarray(2 + nMethods);
83
+ // We only speak no-auth (0x00) to the local client. Chromium always
84
+ // offers it; if a client somehow didn't, reply "no acceptable
85
+ // methods" rather than violate the protocol by selecting unoffered.
86
+ if (!offered.includes(0x00)) {
87
+ try { client.write(Buffer.from([0x05, 0xFF])); } catch (_) {}
88
+ return cleanup();
89
+ }
90
+ client.write(Buffer.from([0x05, 0x00])); // select "no auth"
91
+ phase = 'request';
92
+ }
93
+
94
+ if (phase === 'request') {
95
+ // [0x05, CMD, 0x00, ATYP, ADDR..., PORT(2 BE)]
96
+ if (buf.length < 4) return;
97
+ if (buf[0] !== 0x05) { failReply(client, 0x01); return cleanup(); }
98
+ const cmd = buf[1];
99
+ const atyp = buf[3];
100
+ let host, port, hdrLen;
101
+
102
+ if (atyp === 0x01) { // IPv4
103
+ if (buf.length < 10) return;
104
+ host = `${buf[4]}.${buf[5]}.${buf[6]}.${buf[7]}`;
105
+ port = buf.readUInt16BE(8);
106
+ hdrLen = 10;
107
+ } else if (atyp === 0x03) { // domain
108
+ if (buf.length < 5) return;
109
+ const dLen = buf[4];
110
+ if (buf.length < 7 + dLen) return;
111
+ host = buf.subarray(5, 5 + dLen).toString('utf8');
112
+ port = buf.readUInt16BE(5 + dLen);
113
+ hdrLen = 7 + dLen;
114
+ } else if (atyp === 0x04) { // IPv6
115
+ if (buf.length < 22) return;
116
+ const seg = [];
117
+ for (let i = 0; i < 16; i += 2) seg.push(buf.readUInt16BE(4 + i).toString(16));
118
+ host = seg.join(':');
119
+ port = buf.readUInt16BE(20);
120
+ hdrLen = 22;
121
+ } else {
122
+ failReply(client, 0x08); // address type not supported
123
+ return cleanup();
124
+ }
125
+
126
+ if (cmd !== 0x01) { // only CONNECT
127
+ failReply(client, 0x07);
128
+ return cleanup();
129
+ }
130
+
131
+ // Hand the stream to .pipe() from here. Pause + detach this handler
132
+ // so a data event during the upstream connect can't re-enter.
133
+ phase = 'connecting';
134
+ client.pause();
135
+ client.off('data', onData);
136
+ const early = buf.subarray(hdrLen); // any bytes after the request header
137
+ buf = null;
138
+
139
+ let info;
140
+ try {
141
+ info = await SocksClient.createConnection({
142
+ proxy: {
143
+ host: upstream.host,
144
+ port: upstream.port,
145
+ type: 5,
146
+ userId: upstream.username,
147
+ password: upstream.password || '',
148
+ },
149
+ command: 'connect',
150
+ destination: { host, port },
151
+ timeout: 20000,
152
+ });
153
+ } catch (e) {
154
+ if (forceDebug) {
155
+ console.log(formatLogMessage('proxy', `[socks-relay] upstream connect failed (${host}:${port}): ${e.message}`));
156
+ }
157
+ failReply(client, 0x05); // connection refused
158
+ return cleanup();
159
+ }
160
+
161
+ upstreamSock = info.socket;
162
+ try { upstreamSock.setNoDelay(true); } catch (_) {}
163
+ upstreamSock.on('error', cleanup);
164
+ upstreamSock.on('close', cleanup);
165
+ client.on('error', cleanup);
166
+ client.on('close', cleanup);
167
+
168
+ // SOCKS5 success (BND.ADDR 0.0.0.0:0 — Chromium ignores it for CONNECT)
169
+ client.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
170
+ if (early && early.length) upstreamSock.write(early);
171
+ client.pipe(upstreamSock);
172
+ upstreamSock.pipe(client);
173
+ client.resume();
174
+ phase = 'piping';
175
+ // Negotiation complete — disarm the handshake watchdog so a
176
+ // long-running download isn't killed mid-transfer.
177
+ if (handshakeTimer) { clearTimeout(handshakeTimer); handshakeTimer = null; }
178
+ }
179
+ } catch (e) {
180
+ if (forceDebug) {
181
+ console.log(formatLogMessage('proxy', `[socks-relay] handler error: ${e.message}`));
182
+ }
183
+ cleanup();
184
+ }
185
+ };
186
+
187
+ client.on('data', onData);
188
+ client.on('error', cleanup);
189
+ }
190
+
191
+ // SOCKS5 failure reply (valid only before piping starts).
192
+ function failReply(client, code) {
193
+ try { client.write(Buffer.from([0x05, code, 0x00, 0x01, 0, 0, 0, 0, 0, 0])); } catch (_) {}
194
+ }
195
+
196
+ /**
197
+ * Ensure a relay exists for the given upstream; returns its local port.
198
+ * Idempotent — repeated calls for the same upstream reuse one relay.
199
+ *
200
+ * @param {{host:string,port:number,username:string,password:string}} upstream
201
+ * @param {boolean} forceDebug
202
+ * @returns {Promise<number>} local 127.0.0.1 port the relay listens on
203
+ */
204
+ async function ensureRelay(upstream, forceDebug = false) {
205
+ const key = upstreamKey(upstream);
206
+ const existing = _relays.get(key);
207
+ if (existing) return existing.port;
208
+
209
+ const activeSockets = new Set();
210
+ const server = net.createServer((clientSock) => {
211
+ // Disable Nagle: page scanning is full of small-packet phases (per-origin
212
+ // TLS handshakes, small XHR/API calls, the SOCKS handshake itself).
213
+ // Nagle + delayed-ACK adds ~40ms stalls on those; relays should not.
214
+ try { clientSock.setNoDelay(true); } catch (_) {}
215
+ activeSockets.add(clientSock);
216
+ clientSock.on('close', () => activeSockets.delete(clientSock));
217
+ handleClient(clientSock, upstream, forceDebug);
218
+ });
219
+
220
+ await new Promise((resolve, reject) => {
221
+ const onErr = (e) => reject(e);
222
+ server.once('error', onErr);
223
+ server.listen(0, '127.0.0.1', () => {
224
+ server.removeListener('error', onErr);
225
+ // Keep a listener so a late server error doesn't crash the process.
226
+ server.on('error', (e) => {
227
+ if (forceDebug) console.log(formatLogMessage('proxy', `[socks-relay] server error: ${e.message}`));
228
+ });
229
+ resolve();
230
+ });
231
+ });
232
+
233
+ const port = server.address().port;
234
+ _relays.set(key, { server, port, activeSockets });
235
+ if (forceDebug) {
236
+ console.log(formatLogMessage('proxy', `[socks-relay] 127.0.0.1:${port} -> ${upstream.host}:${upstream.port} (auth user "${upstream.username}")`));
237
+ }
238
+ return port;
239
+ }
240
+
241
+ /**
242
+ * Sync lookup of an already-started relay's port. Returns null if no relay
243
+ * has been started for this upstream (caller should have called ensureRelay
244
+ * upfront).
245
+ */
246
+ function getRelayPort(upstream) {
247
+ const r = _relays.get(upstreamKey(upstream));
248
+ return r ? r.port : null;
249
+ }
250
+
251
+ /**
252
+ * Tear down every relay: destroy in-flight sockets, close listeners.
253
+ * Safe to call multiple times.
254
+ */
255
+ async function closeAllRelays(forceDebug = false) {
256
+ for (const [key, r] of _relays) {
257
+ for (const s of r.activeSockets) { try { s.destroy(); } catch (_) {} }
258
+ await new Promise((res) => {
259
+ try { r.server.close(() => res()); } catch (_) { res(); }
260
+ });
261
+ if (forceDebug) console.log(formatLogMessage('proxy', `[socks-relay] closed relay for ${key}`));
262
+ }
263
+ _relays.clear();
264
+ }
265
+
266
+ module.exports = { ensureRelay, getRelayPort, closeAllRelays };