@fanboynz/network-scanner 2.0.66 → 3.0.0
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/.github/workflows/npm-publish.yml +134 -10
- package/CHANGELOG.md +135 -0
- package/CLAUDE.md +18 -7
- package/README.md +12 -4
- package/lib/adblock-rust.js +23 -18
- package/lib/adblock.js +127 -82
- package/lib/browserexit.js +210 -200
- package/lib/browserhealth.js +84 -60
- package/lib/cdp.js +103 -81
- package/lib/clear_sitedata.js +61 -159
- package/lib/cloudflare.js +579 -409
- package/lib/colorize.js +29 -12
- package/lib/compare.js +16 -8
- package/lib/compress.js +2 -1
- package/lib/curl.js +287 -220
- package/lib/domain-cache.js +87 -40
- package/lib/dry-run.js +137 -194
- package/lib/fingerprint.js +20 -18
- package/lib/flowproxy.js +391 -188
- package/lib/ghost-cursor.js +8 -7
- package/lib/grep.js +248 -171
- package/lib/ignore_similar.js +70 -124
- package/lib/interaction.js +132 -235
- package/lib/nettools.js +309 -87
- package/lib/openvpn_vpn.js +12 -11
- package/lib/output.js +92 -59
- package/lib/post-processing.js +216 -162
- package/lib/redirect.js +46 -30
- package/lib/referrer.js +158 -165
- package/lib/searchstring.js +290 -381
- package/lib/smart-cache.js +141 -91
- package/lib/socks-relay.js +8 -7
- package/lib/spawn-async.js +137 -0
- package/lib/validate_rules.js +188 -176
- package/lib/wireguard_vpn.js +111 -117
- package/nwss.js +740 -156
- package/package.json +4 -4
package/lib/wireguard_vpn.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// Per-site VPN configuration for network scanner
|
|
3
3
|
// Manages WireGuard interfaces, routing, and lifecycle
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
|
-
const { formatLogMessage } = require('./colorize');
|
|
9
|
+
const { formatLogMessage, messageColors } = require('./colorize');
|
|
10
|
+
const VPN_TAG = messageColors.processing('[vpn]');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Fetch external IP address through the active tunnel
|
|
@@ -15,10 +17,15 @@ const { formatLogMessage } = require('./colorize');
|
|
|
15
17
|
function getExternalIP(interfaceName) {
|
|
16
18
|
const services = ['https://api.ipify.org', 'https://ifconfig.me/ip', 'https://icanhazip.com'];
|
|
17
19
|
for (const service of services) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const args = ['-s', '-m', '5'];
|
|
21
|
+
if (interfaceName) args.push('--interface', interfaceName);
|
|
22
|
+
args.push(service);
|
|
23
|
+
// spawnSync (no shell) so a malicious interfaceName like
|
|
24
|
+
// "wg-foo; rm -rf ~" can't be split into a second command.
|
|
25
|
+
const result = spawnSync('curl', args, { encoding: 'utf8', timeout: 8000 });
|
|
26
|
+
if (result.status === 0 && result.stdout) {
|
|
27
|
+
return result.stdout.trim();
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
return null;
|
|
24
31
|
}
|
|
@@ -29,24 +36,6 @@ const activeInterfaces = new Map();
|
|
|
29
36
|
// Temp config directory for inline configs
|
|
30
37
|
const TEMP_CONFIG_DIR = '/tmp/nwss-wireguard';
|
|
31
38
|
|
|
32
|
-
/**
|
|
33
|
-
* Validate WireGuard availability on the system
|
|
34
|
-
* @returns {Object} { isAvailable, version, error }
|
|
35
|
-
*/
|
|
36
|
-
function validateWireGuardAvailability() {
|
|
37
|
-
try {
|
|
38
|
-
const version = execSync('wg --version 2>&1', { encoding: 'utf8' }).trim();
|
|
39
|
-
// Check wg-quick is also available
|
|
40
|
-
execSync('which wg-quick', { encoding: 'utf8' });
|
|
41
|
-
return { isAvailable: true, version };
|
|
42
|
-
} catch (error) {
|
|
43
|
-
return {
|
|
44
|
-
isAvailable: false,
|
|
45
|
-
error: 'WireGuard not found. Install with: sudo apt install wireguard'
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
39
|
/**
|
|
51
40
|
* Check if running with sufficient privileges
|
|
52
41
|
* @returns {boolean}
|
|
@@ -69,12 +58,24 @@ function resolveInterfaceName(vpnConfig) {
|
|
|
69
58
|
return vpnConfig.interface;
|
|
70
59
|
}
|
|
71
60
|
if (vpnConfig.config) {
|
|
72
|
-
// Extract name from /etc/wireguard/wg-example.conf
|
|
61
|
+
// Extract name from /etc/wireguard/wg-example.conf → wg-example
|
|
73
62
|
return path.basename(vpnConfig.config, '.conf');
|
|
74
63
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
// Inline-only config without an explicit interface: derive a stable
|
|
65
|
+
// name from a hash of the content so connect and disconnect resolve
|
|
66
|
+
// to the same name across calls. The old `wg-nwss${activeInterfaces.size}`
|
|
67
|
+
// used the live Map size, so disconnect computed a DIFFERENT name
|
|
68
|
+
// than connect did (size had grown in between) and silently failed
|
|
69
|
+
// to find the entry — the interface would leak until disconnectAll.
|
|
70
|
+
//
|
|
71
|
+
// Truncated SHA-1 to 8 hex chars keeps the total under Linux's
|
|
72
|
+
// 15-char IFNAMSIZ limit ('wg-nwss' = 7 + 8 = 15).
|
|
73
|
+
if (vpnConfig.config_inline) {
|
|
74
|
+
const hash = crypto.createHash('sha1').update(vpnConfig.config_inline).digest('hex').slice(0, 8);
|
|
75
|
+
return `wg-nwss${hash}`;
|
|
76
|
+
}
|
|
77
|
+
// Last resort — should be unreachable if validation ran first.
|
|
78
|
+
return 'wg-nwss-unknown';
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -103,17 +104,24 @@ function writeInlineConfig(interfaceName, configContent) {
|
|
|
103
104
|
function interfaceUp(configPath, interfaceName, forceDebug = false) {
|
|
104
105
|
if (activeInterfaces.has(interfaceName)) {
|
|
105
106
|
if (forceDebug) {
|
|
106
|
-
console.log(formatLogMessage('debug',
|
|
107
|
+
console.log(formatLogMessage('debug', `${VPN_TAG} Interface ${interfaceName} already active`));
|
|
107
108
|
}
|
|
108
109
|
return { success: true, interface: interfaceName, alreadyActive: true };
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
try {
|
|
112
|
-
// wg-quick accepts a config path or interface name in /etc/wireguard
|
|
113
|
-
|
|
113
|
+
// wg-quick accepts a config path or interface name in /etc/wireguard/.
|
|
114
|
+
// spawnSync with arg array (no shell) — configPath comes from user
|
|
115
|
+
// JSON, so naive `sudo wg-quick up "${configPath}"` was vulnerable
|
|
116
|
+
// to a `";rm -rf ~;"` payload escaping the quotes.
|
|
117
|
+
const upRes = spawnSync('sudo', ['wg-quick', 'up', configPath], {
|
|
114
118
|
encoding: 'utf8',
|
|
115
119
|
timeout: 15000
|
|
116
120
|
});
|
|
121
|
+
if (upRes.error) throw upRes.error;
|
|
122
|
+
if (upRes.status !== 0) {
|
|
123
|
+
throw new Error((upRes.stderr || '').trim() || `wg-quick up exited with status ${upRes.status}`);
|
|
124
|
+
}
|
|
117
125
|
|
|
118
126
|
activeInterfaces.set(interfaceName, {
|
|
119
127
|
configPath,
|
|
@@ -122,11 +130,19 @@ function interfaceUp(configPath, interfaceName, forceDebug = false) {
|
|
|
122
130
|
});
|
|
123
131
|
|
|
124
132
|
if (forceDebug) {
|
|
125
|
-
console.log(formatLogMessage('debug',
|
|
133
|
+
console.log(formatLogMessage('debug', `${VPN_TAG} Interface ${interfaceName} is up`));
|
|
134
|
+
// Only fetch the external IP when debug-logging would actually
|
|
135
|
+
// display it — getExternalIP runs 3 sequential 8s-timeout curls
|
|
136
|
+
// (~24s worst case of blocking event loop). The result was
|
|
137
|
+
// previously included in the return shape but no caller read
|
|
138
|
+
// it; the work was pure waste outside debug runs.
|
|
139
|
+
const externalIP = getExternalIP(interfaceName);
|
|
140
|
+
if (externalIP) {
|
|
141
|
+
console.log(formatLogMessage('debug', `${VPN_TAG} ${interfaceName} external IP: ${externalIP}`));
|
|
142
|
+
}
|
|
126
143
|
}
|
|
127
144
|
|
|
128
|
-
|
|
129
|
-
return { success: true, interface: interfaceName, externalIP };
|
|
145
|
+
return { success: true, interface: interfaceName };
|
|
130
146
|
} catch (error) {
|
|
131
147
|
return {
|
|
132
148
|
success: false,
|
|
@@ -149,21 +165,20 @@ function interfaceDown(interfaceName, forceDebug = false) {
|
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
try {
|
|
152
|
-
|
|
168
|
+
// spawnSync with arg array (see interfaceUp comment for rationale).
|
|
169
|
+
const downRes = spawnSync('sudo', ['wg-quick', 'down', info.configPath], {
|
|
153
170
|
encoding: 'utf8',
|
|
154
171
|
timeout: 10000
|
|
155
172
|
});
|
|
173
|
+
if (downRes.error) throw downRes.error;
|
|
174
|
+
if (downRes.status !== 0) {
|
|
175
|
+
throw new Error((downRes.stderr || '').trim() || `wg-quick down exited with status ${downRes.status}`);
|
|
176
|
+
}
|
|
156
177
|
|
|
157
178
|
activeInterfaces.delete(interfaceName);
|
|
158
179
|
|
|
159
|
-
// Clean up temp config if it was inline
|
|
160
|
-
const tempPath = path.join(TEMP_CONFIG_DIR, `${interfaceName}.conf`);
|
|
161
|
-
if (fs.existsSync(tempPath)) {
|
|
162
|
-
try { fs.unlinkSync(tempPath); } catch {}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
180
|
if (forceDebug) {
|
|
166
|
-
console.log(formatLogMessage('debug',
|
|
181
|
+
console.log(formatLogMessage('debug', `${VPN_TAG} Interface ${interfaceName} is down`));
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
return { success: true };
|
|
@@ -171,6 +186,17 @@ function interfaceDown(interfaceName, forceDebug = false) {
|
|
|
171
186
|
// Force remove from tracking even if wg-quick fails
|
|
172
187
|
activeInterfaces.delete(interfaceName);
|
|
173
188
|
return { success: false, error: error.message.trim() };
|
|
189
|
+
} finally {
|
|
190
|
+
// Clean up temp config regardless of wg-quick outcome — a leaked
|
|
191
|
+
// .conf in TEMP_CONFIG_DIR could collide on a re-connect with the
|
|
192
|
+
// same hash-derived interface name, especially after a wg-quick
|
|
193
|
+
// down failure where the kernel interface might persist briefly.
|
|
194
|
+
// Was previously only inside the try block, so failure paths
|
|
195
|
+
// leaked the temp file.
|
|
196
|
+
const tempPath = path.join(TEMP_CONFIG_DIR, `${interfaceName}.conf`);
|
|
197
|
+
if (fs.existsSync(tempPath)) {
|
|
198
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
199
|
+
}
|
|
174
200
|
}
|
|
175
201
|
}
|
|
176
202
|
|
|
@@ -183,21 +209,32 @@ function interfaceDown(interfaceName, forceDebug = false) {
|
|
|
183
209
|
*/
|
|
184
210
|
function checkConnection(interfaceName, testHost = '1.1.1.1', forceDebug = false) {
|
|
185
211
|
try {
|
|
186
|
-
// Check interface exists
|
|
187
|
-
|
|
212
|
+
// Check interface exists. spawnSync with arg array — interfaceName
|
|
213
|
+
// can come from user JSON (siteConfig.vpn.interface) so the old
|
|
214
|
+
// shell-interpolated `execSync(\`ip link show ${interfaceName}\`)`
|
|
215
|
+
// was injection-vulnerable.
|
|
216
|
+
const linkRes = spawnSync('ip', ['link', 'show', interfaceName], { encoding: 'utf8', timeout: 3000 });
|
|
217
|
+
if (linkRes.status !== 0) {
|
|
218
|
+
throw new Error((linkRes.stderr || '').trim() || `ip link show failed for ${interfaceName}`);
|
|
219
|
+
}
|
|
188
220
|
|
|
189
|
-
// Ping through the specific interface
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
221
|
+
// Ping through the specific interface. testHost defaults to '1.1.1.1'
|
|
222
|
+
// but can be overridden by user config — same injection concern.
|
|
223
|
+
const pingRes = spawnSync('ping', ['-c', '1', '-W', '5', '-I', interfaceName, testHost], {
|
|
224
|
+
encoding: 'utf8', timeout: 8000
|
|
225
|
+
});
|
|
226
|
+
if (pingRes.status !== 0) {
|
|
227
|
+
// Combine stderr + stdout (no shell `2>&1` available with spawnSync)
|
|
228
|
+
throw new Error((pingRes.stderr || pingRes.stdout || '').split('\n')[0] || `ping failed for ${testHost}`);
|
|
229
|
+
}
|
|
230
|
+
const result = pingRes.stdout;
|
|
194
231
|
|
|
195
232
|
const latencyMatch = result.match(/time=([0-9.]+)\s*ms/);
|
|
196
233
|
const latencyMs = latencyMatch ? parseFloat(latencyMatch[1]) : null;
|
|
197
234
|
|
|
198
235
|
if (forceDebug) {
|
|
199
236
|
console.log(formatLogMessage('debug',
|
|
200
|
-
|
|
237
|
+
`${VPN_TAG} ${interfaceName} connected (${latencyMs ? latencyMs + 'ms' : 'ok'})`
|
|
201
238
|
));
|
|
202
239
|
}
|
|
203
240
|
|
|
@@ -205,44 +242,13 @@ function checkConnection(interfaceName, testHost = '1.1.1.1', forceDebug = false
|
|
|
205
242
|
} catch (error) {
|
|
206
243
|
if (forceDebug) {
|
|
207
244
|
console.log(formatLogMessage('debug',
|
|
208
|
-
|
|
245
|
+
`${VPN_TAG} ${interfaceName} health check failed: ${error.message.split('\n')[0]}`
|
|
209
246
|
));
|
|
210
247
|
}
|
|
211
248
|
return { connected: false, error: error.message.split('\n')[0] };
|
|
212
249
|
}
|
|
213
250
|
}
|
|
214
251
|
|
|
215
|
-
/**
|
|
216
|
-
* Get WireGuard status for an interface
|
|
217
|
-
* @param {string} interfaceName - Interface name
|
|
218
|
-
* @returns {Object} Parsed wg show output
|
|
219
|
-
*/
|
|
220
|
-
function getInterfaceStatus(interfaceName) {
|
|
221
|
-
try {
|
|
222
|
-
const output = execSync(`wg show ${interfaceName}`, {
|
|
223
|
-
encoding: 'utf8',
|
|
224
|
-
timeout: 5000
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const status = { interface: interfaceName, raw: output };
|
|
228
|
-
|
|
229
|
-
// Parse key fields
|
|
230
|
-
const endpointMatch = output.match(/endpoint:\s*(.+)/);
|
|
231
|
-
const transferMatch = output.match(/transfer:\s*(.+)/);
|
|
232
|
-
const handshakeMatch = output.match(/latest handshake:\s*(.+)/);
|
|
233
|
-
const allowedMatch = output.match(/allowed ips:\s*(.+)/);
|
|
234
|
-
|
|
235
|
-
if (endpointMatch) status.endpoint = endpointMatch[1].trim();
|
|
236
|
-
if (transferMatch) status.transfer = transferMatch[1].trim();
|
|
237
|
-
if (handshakeMatch) status.latestHandshake = handshakeMatch[1].trim();
|
|
238
|
-
if (allowedMatch) status.allowedIps = allowedMatch[1].trim();
|
|
239
|
-
|
|
240
|
-
return status;
|
|
241
|
-
} catch (error) {
|
|
242
|
-
return { interface: interfaceName, error: error.message.split('\n')[0] };
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
252
|
/**
|
|
247
253
|
* Parse and validate a VPN site config
|
|
248
254
|
* @param {Object|string} vpnConfig - VPN config from site JSON
|
|
@@ -258,6 +264,17 @@ function normalizeVpnConfig(vpnConfig) {
|
|
|
258
264
|
return null;
|
|
259
265
|
}
|
|
260
266
|
|
|
267
|
+
// Accept non-negative integers only — rejects:
|
|
268
|
+
// - undefined/null/false (would have hit '|| 2' fallback anyway)
|
|
269
|
+
// - strings like "3" (the old `|| 2` accepted those, then
|
|
270
|
+
// `vpnConfig.max_retries + 1` downstream string-concatenated
|
|
271
|
+
// to "31" and ran 31 retry attempts instead of 4)
|
|
272
|
+
// - negative numbers / non-integers
|
|
273
|
+
// Explicit 0 IS accepted now ("no retries, fail fast") — the old
|
|
274
|
+
// `|| 2` treated 0 as falsy and silently substituted 2.
|
|
275
|
+
const mr = vpnConfig.max_retries;
|
|
276
|
+
const max_retries = (typeof mr === 'number' && Number.isInteger(mr) && mr >= 0) ? mr : 2;
|
|
277
|
+
|
|
261
278
|
return {
|
|
262
279
|
config: vpnConfig.config || null,
|
|
263
280
|
config_inline: vpnConfig.config_inline || null,
|
|
@@ -265,7 +282,7 @@ function normalizeVpnConfig(vpnConfig) {
|
|
|
265
282
|
health_check: vpnConfig.health_check !== false,
|
|
266
283
|
test_host: vpnConfig.test_host || '1.1.1.1',
|
|
267
284
|
retry: vpnConfig.retry !== false,
|
|
268
|
-
max_retries
|
|
285
|
+
max_retries
|
|
269
286
|
};
|
|
270
287
|
}
|
|
271
288
|
|
|
@@ -368,7 +385,7 @@ async function connectForSite(siteConfig, forceDebug = false) {
|
|
|
368
385
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
369
386
|
if (forceDebug && attempt > 1) {
|
|
370
387
|
console.log(formatLogMessage('debug',
|
|
371
|
-
|
|
388
|
+
`${VPN_TAG} Retry ${attempt - 1}/${vpnConfig.max_retries} for ${interfaceName}`
|
|
372
389
|
));
|
|
373
390
|
}
|
|
374
391
|
|
|
@@ -448,7 +465,7 @@ function disconnectForSite(siteConfig, forceDebug = false) {
|
|
|
448
465
|
|
|
449
466
|
if (forceDebug) {
|
|
450
467
|
console.log(formatLogMessage('debug',
|
|
451
|
-
|
|
468
|
+
`${VPN_TAG} ${interfaceName} still used by ${info.sites.size} site(s), keeping up`
|
|
452
469
|
));
|
|
453
470
|
}
|
|
454
471
|
|
|
@@ -480,47 +497,24 @@ function disconnectAll(forceDebug = false) {
|
|
|
480
497
|
|
|
481
498
|
if (forceDebug && results.tornDown > 0) {
|
|
482
499
|
console.log(formatLogMessage('debug',
|
|
483
|
-
|
|
500
|
+
`${VPN_TAG} Disconnected ${results.tornDown} interface(s)`
|
|
484
501
|
));
|
|
485
502
|
}
|
|
486
503
|
|
|
487
504
|
return results;
|
|
488
505
|
}
|
|
489
506
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
for (const [name, info] of activeInterfaces) {
|
|
498
|
-
const status = getInterfaceStatus(name);
|
|
499
|
-
interfaces.push({
|
|
500
|
-
name,
|
|
501
|
-
configPath: info.configPath,
|
|
502
|
-
uptime: Math.round((Date.now() - info.startedAt) / 1000),
|
|
503
|
-
sites: Array.from(info.sites),
|
|
504
|
-
...status
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return interfaces;
|
|
509
|
-
}
|
|
510
|
-
|
|
507
|
+
// Public surface used by nwss.js. Internal helpers (checkConnection,
|
|
508
|
+
// interfaceUp, interfaceDown, resolveInterfaceName, hasRootPrivileges,
|
|
509
|
+
// getExternalIP, writeInlineConfig) stay module-private — none had
|
|
510
|
+
// external callers. validateWireGuardAvailability, getInterfaceStatus,
|
|
511
|
+
// and getActiveInterfaces were removed entirely (zero callers anywhere,
|
|
512
|
+
// including no internal ones once their downstream consumers were
|
|
513
|
+
// pruned).
|
|
511
514
|
module.exports = {
|
|
512
|
-
validateWireGuardAvailability,
|
|
513
515
|
validateVpnConfig,
|
|
514
516
|
normalizeVpnConfig,
|
|
515
517
|
connectForSite,
|
|
516
518
|
disconnectForSite,
|
|
517
|
-
disconnectAll
|
|
518
|
-
checkConnection,
|
|
519
|
-
getInterfaceStatus,
|
|
520
|
-
getActiveInterfaces,
|
|
521
|
-
// Low-level (for testing or advanced use)
|
|
522
|
-
interfaceUp,
|
|
523
|
-
interfaceDown,
|
|
524
|
-
resolveInterfaceName,
|
|
525
|
-
hasRootPrivileges
|
|
519
|
+
disconnectAll
|
|
526
520
|
};
|