@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.
- package/lib/cloudflare.js +225 -102
- package/lib/nettools.js +85 -57
- package/lib/proxy.js +105 -7
- package/lib/redirect.js +38 -8
- package/lib/socks-relay.js +266 -0
- package/nwss.js +365 -105
- package/package.json +4 -3
- package/scanner-script-org.js +0 -588
|
@@ -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 };
|