@fanboynz/network-scanner 3.0.3 → 3.1.2
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/CHANGELOG.md +53 -0
- package/lib/adblock-rust.js +17 -4
- package/lib/adblock.js +92 -15
- package/lib/browserhealth.js +41 -100
- package/lib/cdp.js +68 -34
- package/lib/clear_sitedata.js +68 -20
- package/lib/compress.js +26 -58
- package/lib/curl.js +44 -22
- package/lib/domain-cache.js +8 -57
- package/lib/dry-run.js +9 -4
- package/lib/fingerprint.js +599 -129
- package/lib/fingerprint.md +94 -0
- package/lib/interaction.js +262 -26
- package/lib/nettools.js +47 -76
- package/lib/openvpn_vpn.js +116 -35
- package/lib/proxy.js +6 -2
- package/lib/searchstring.js +15 -237
- package/lib/smart-cache.js +9 -1
- package/lib/socks-relay.js +14 -9
- package/lib/validate_rules.js +285 -3
- package/lib/wireguard_vpn.js +64 -12
- package/nwss.js +557 -220
- package/package.json +1 -1
- package/regex-tool/index.html +321 -628
package/lib/nettools.js
CHANGED
|
@@ -561,8 +561,19 @@ async function whoisLookup(domain = '', timeout = 10000, whoisServer = '', debug
|
|
|
561
561
|
|
|
562
562
|
const { stdout, stderr } = await execFileWithTimeout('whois', whoisArgs, timeout);
|
|
563
563
|
const duration = Date.now() - startTime;
|
|
564
|
-
|
|
565
|
-
|
|
564
|
+
|
|
565
|
+
// Treat stderr as failure ONLY when there's no usable stdout. Many whois
|
|
566
|
+
// servers/clients emit non-fatal notices to stderr — referral/redirect
|
|
567
|
+
// lines, rate-limit and GDPR "data redacted" banners, registry
|
|
568
|
+
// disclaimers — while returning the real record on stdout. The old "any
|
|
569
|
+
// stderr -> fail" path discarded those valid records (and triggered
|
|
570
|
+
// needless fallback retries in whoisLookupWithRetry). When stdout has
|
|
571
|
+
// content we prefer it and treat the lookup as successful; the downstream
|
|
572
|
+
// term match is the real gate, so this can't manufacture a match. stderr
|
|
573
|
+
// is still surfaced in debug logging below for visibility.
|
|
574
|
+
const hasUsableStdout = !!(stdout && stdout.trim());
|
|
575
|
+
|
|
576
|
+
if (!hasUsableStdout && stderr && stderr.trim()) {
|
|
566
577
|
if (debugMode) {
|
|
567
578
|
if (logFunc) {
|
|
568
579
|
logFunc(`${messageColors.highlight('[whois]')} Lookup failed for ${cleanDomain} after ${duration}ms`);
|
|
@@ -604,6 +615,14 @@ async function whoisLookup(domain = '', timeout = 10000, whoisServer = '', debug
|
|
|
604
615
|
console.log(formatLogMessage('debug', `${messageColors.highlight('[whois]')} Server: ${selectedServer || 'default'}`));
|
|
605
616
|
console.log(formatLogMessage('debug', `${messageColors.highlight('[whois]')} Output length: ${stdout.length} characters`));
|
|
606
617
|
}
|
|
618
|
+
// Non-fatal stderr alongside usable stdout — kept visible so a real
|
|
619
|
+
// problem (truncation, partial referral) is still diagnosable even
|
|
620
|
+
// though we're treating the lookup as a success.
|
|
621
|
+
if (stderr && stderr.trim()) {
|
|
622
|
+
const note = `${messageColors.highlight('[whois]')} Non-fatal stderr (stdout used): ${stderr.trim()}`;
|
|
623
|
+
if (logFunc) logFunc(note);
|
|
624
|
+
else console.log(formatLogMessage('debug', note));
|
|
625
|
+
}
|
|
607
626
|
}
|
|
608
627
|
|
|
609
628
|
return {
|
|
@@ -1021,66 +1040,6 @@ async function digLookup(domain = '', recordType = 'A', timeout = 5000) {
|
|
|
1021
1040
|
}
|
|
1022
1041
|
}
|
|
1023
1042
|
|
|
1024
|
-
/**
|
|
1025
|
-
* Checks if whois output contains all specified search terms (AND logic)
|
|
1026
|
-
* @param {string} whoisOutput - The whois lookup output
|
|
1027
|
-
* @param {Array<string>} searchTerms - Array of terms that must all be present
|
|
1028
|
-
* @returns {boolean} True if all terms are found
|
|
1029
|
-
*/
|
|
1030
|
-
function checkWhoisTerms(whoisOutput, searchTerms) {
|
|
1031
|
-
if (!searchTerms || !Array.isArray(searchTerms) || searchTerms.length === 0) {
|
|
1032
|
-
return false;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const lowerOutput = whoisOutput.toLowerCase();
|
|
1036
|
-
return searchTerms.every(term => lowerOutput.includes(term.toLowerCase()));
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* Checks if whois output contains any of the specified search terms (OR logic)
|
|
1041
|
-
* @param {string} whoisOutput - The whois lookup output
|
|
1042
|
-
* @param {Array<string>} searchTerms - Array of terms where at least one must be present
|
|
1043
|
-
* @returns {boolean} True if any term is found
|
|
1044
|
-
*/
|
|
1045
|
-
function checkWhoisTermsOr(whoisOutput, searchTerms) {
|
|
1046
|
-
if (!searchTerms || !Array.isArray(searchTerms) || searchTerms.length === 0) {
|
|
1047
|
-
return false;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const lowerOutput = whoisOutput.toLowerCase();
|
|
1051
|
-
return searchTerms.some(term => lowerOutput.includes(term.toLowerCase()));
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/**
|
|
1055
|
-
* Checks if dig output contains all specified search terms (AND logic)
|
|
1056
|
-
* @param {string} digOutput - The dig lookup output
|
|
1057
|
-
* @param {Array<string>} searchTerms - Array of terms that must all be present
|
|
1058
|
-
* @returns {boolean} True if all terms are found
|
|
1059
|
-
*/
|
|
1060
|
-
function checkDigTerms(digOutput, searchTerms) {
|
|
1061
|
-
if (!searchTerms || !Array.isArray(searchTerms) || searchTerms.length === 0) {
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
const lowerOutput = digOutput.toLowerCase();
|
|
1066
|
-
return searchTerms.every(term => lowerOutput.includes(term.toLowerCase()));
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* Checks if dig output contains any of the specified search terms (OR logic)
|
|
1071
|
-
* @param {string} digOutput - The dig lookup output
|
|
1072
|
-
* @param {Array<string>} searchTerms - Array of terms where at least one must be present
|
|
1073
|
-
* @returns {boolean} True if any term is found
|
|
1074
|
-
*/
|
|
1075
|
-
function checkDigTermsOr(digOutput, searchTerms) {
|
|
1076
|
-
if (!searchTerms || !Array.isArray(searchTerms) || searchTerms.length === 0) {
|
|
1077
|
-
return false;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
const lowerOutput = digOutput.toLowerCase();
|
|
1081
|
-
return searchTerms.some(term => lowerOutput.includes(term.toLowerCase()));
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
1043
|
/**
|
|
1085
1044
|
* Enhanced dry run callback factory for better nettools reporting
|
|
1086
1045
|
* @param {Map} matchedDomains - The matched domains collection
|
|
@@ -1221,7 +1180,20 @@ function createNetToolsHandler(config) {
|
|
|
1221
1180
|
const digDedupeKey = `${digDomain}:${digConfigKey}`;
|
|
1222
1181
|
const needsWhoisLookup = (hasWhois || hasWhoisOr) && !processedWhoisDomains.has(whoisDedupeKey);
|
|
1223
1182
|
const needsDigLookup = (hasDig || hasDigOr) && !processedDigDomains.has(digDedupeKey);
|
|
1224
|
-
|
|
1183
|
+
|
|
1184
|
+
// Claim the dedupe keys NOW, synchronously, before executeNetToolsLookup
|
|
1185
|
+
// hits its first await. These Sets are shared across all concurrent URL
|
|
1186
|
+
// handlers, so the .has() checks above and these .add() claims must sit in
|
|
1187
|
+
// the same uninterrupted synchronous span to be atomic. The whois claim
|
|
1188
|
+
// was already safe (its add ran before any await), but the dig claim used
|
|
1189
|
+
// to live AFTER the whois lookup's await — opening a window where a second
|
|
1190
|
+
// handler for the same domain passed the dig check before the first
|
|
1191
|
+
// claimed it, running dig twice. Claiming both here closes that window.
|
|
1192
|
+
// Matches the existing claim-before-lookup semantics (no rollback on
|
|
1193
|
+
// failure: a failed lookup still suppresses retries for the TTL).
|
|
1194
|
+
if (needsWhoisLookup) processedWhoisDomains.add(whoisDedupeKey);
|
|
1195
|
+
if (needsDigLookup) processedDigDomains.add(digDedupeKey);
|
|
1196
|
+
|
|
1225
1197
|
// Skip if we don't need to perform any lookups
|
|
1226
1198
|
if (!needsWhoisLookup && !needsDigLookup) {
|
|
1227
1199
|
if (forceDebug) {
|
|
@@ -1320,9 +1292,9 @@ function createNetToolsHandler(config) {
|
|
|
1320
1292
|
|
|
1321
1293
|
// Perform whois lookup if either whois or whois-or is configured
|
|
1322
1294
|
if (needsWhoisLookup) {
|
|
1323
|
-
//
|
|
1324
|
-
|
|
1325
|
-
|
|
1295
|
+
// Dedupe key already claimed up-front (before the first await) to keep
|
|
1296
|
+
// the has()/add() span atomic across concurrent handlers.
|
|
1297
|
+
|
|
1326
1298
|
// Check whois cache first - cache key includes server for accuracy
|
|
1327
1299
|
const selectedServer = selectWhoisServer(whoisServer, whoisServerMode);
|
|
1328
1300
|
const whoisCacheKey = `${whoisRootDomain}-${(selectedServer && selectedServer !== '') ? selectedServer : 'default'}`;
|
|
@@ -1438,11 +1410,10 @@ function createNetToolsHandler(config) {
|
|
|
1438
1410
|
if (whoisResult) {
|
|
1439
1411
|
|
|
1440
1412
|
if (whoisResult.success) {
|
|
1441
|
-
// Lowercase the output ONCE
|
|
1442
|
-
// each
|
|
1443
|
-
// re-
|
|
1444
|
-
// here
|
|
1445
|
-
// a single allocation.
|
|
1413
|
+
// Lowercase the output ONCE. The AND check, OR check, and
|
|
1414
|
+
// matched-term find below each need a lowercased copy; doing it
|
|
1415
|
+
// per-check would re-allocate a multi-KB string each time, so
|
|
1416
|
+
// pre-lower here and let all three share a single allocation.
|
|
1446
1417
|
const whoisOutputLower = whoisResult.output.toLowerCase();
|
|
1447
1418
|
|
|
1448
1419
|
// Check AND terms if configured
|
|
@@ -1578,9 +1549,10 @@ function createNetToolsHandler(config) {
|
|
|
1578
1549
|
|
|
1579
1550
|
// Perform dig lookup if configured
|
|
1580
1551
|
if (needsDigLookup) {
|
|
1581
|
-
//
|
|
1582
|
-
|
|
1583
|
-
|
|
1552
|
+
// Dedupe key already claimed up-front (before the first await) to keep
|
|
1553
|
+
// the has()/add() span atomic across concurrent handlers — this used
|
|
1554
|
+
// to claim here, after the whois await, which left the race window.
|
|
1555
|
+
|
|
1584
1556
|
if (forceDebug) {
|
|
1585
1557
|
const digTypes = [];
|
|
1586
1558
|
if (hasDig) digTypes.push('dig-and');
|
|
@@ -1826,8 +1798,7 @@ function createNetToolsHandler(config) {
|
|
|
1826
1798
|
|
|
1827
1799
|
// Public surface kept narrow on purpose -- only what nwss.js actually
|
|
1828
1800
|
// imports (verified via repo-wide grep). Internal helpers
|
|
1829
|
-
// (whoisLookup, whoisLookupWithRetry, digLookup,
|
|
1830
|
-
// checkWhoisTermsOr, checkDigTerms, checkDigTermsOr, selectWhoisServer,
|
|
1801
|
+
// (whoisLookup, whoisLookupWithRetry, digLookup, selectWhoisServer,
|
|
1831
1802
|
// getCommonWhoisServers, suggestWhoisServers, execFileWithTimeout,
|
|
1832
1803
|
// markResolved, digOutputIndicatesResolution, loadDiskCache,
|
|
1833
1804
|
// saveDiskCache, enforceCacheCap, stripAnsiColors) stay as module-local
|
package/lib/openvpn_vpn.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// VPN tunnel � not just the site that requested it. For isolated
|
|
9
9
|
// per-site VPN with concurrency, a SOCKS proxy approach is needed.
|
|
10
10
|
|
|
11
|
-
const { execSync, spawn } = require('child_process');
|
|
11
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
const fs = require('fs');
|
|
13
14
|
const path = require('path');
|
|
14
15
|
const { formatLogMessage, messageColors } = require('./colorize');
|
|
@@ -22,10 +23,17 @@ const OPENVPN_TAG = messageColors.processing('[openvpn]');
|
|
|
22
23
|
function getExternalIP(tunDevice) {
|
|
23
24
|
const services = ['https://api.ipify.org', 'https://ifconfig.me/ip', 'https://icanhazip.com'];
|
|
24
25
|
for (const service of services) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// spawnSync with arg array (no shell) — tunDevice flows from
|
|
27
|
+
// findTunDevice (kernel-assigned /sys/class/net names, practically
|
|
28
|
+
// safe) but the pattern with execSync + interpolation was bad style.
|
|
29
|
+
// Match wireguard_vpn.js's spawn-array approach for consistency.
|
|
30
|
+
const args = ['-s', '-m', '5'];
|
|
31
|
+
if (tunDevice) args.push('--interface', tunDevice);
|
|
32
|
+
args.push(service);
|
|
33
|
+
const result = spawnSync('curl', args, { encoding: 'utf8', timeout: 8000 });
|
|
34
|
+
if (result.status === 0 && result.stdout) {
|
|
35
|
+
return result.stdout.trim();
|
|
36
|
+
}
|
|
29
37
|
}
|
|
30
38
|
return null;
|
|
31
39
|
}
|
|
@@ -127,7 +135,13 @@ function checkTunDevice() {
|
|
|
127
135
|
* Ensure temp directory exists with secure permissions
|
|
128
136
|
*/
|
|
129
137
|
function ensureTempDir() {
|
|
130
|
-
|
|
138
|
+
// 0o700 matches wireguard_vpn.js — other users on the box can't list
|
|
139
|
+
// the dir to discover which connection names exist. Individual files
|
|
140
|
+
// inside are already 0o600 so contents were safe before, but directory
|
|
141
|
+
// listing leaked the connection-name list. Note: mode is only applied
|
|
142
|
+
// on creation; an existing dir from a prior run with mode 0o755 keeps
|
|
143
|
+
// its mode until disconnectAll's rm -rf + next session creates fresh.
|
|
144
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true, mode: 0o700 });
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/**
|
|
@@ -169,8 +183,19 @@ function resolveConnectionName(vpnConfig) {
|
|
|
169
183
|
if (vpnConfig.config) {
|
|
170
184
|
return path.basename(vpnConfig.config, '.ovpn');
|
|
171
185
|
}
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
// Inline-only config without explicit name: derive a stable name from a
|
|
187
|
+
// hash of the content so connect and disconnect resolve to the same name
|
|
188
|
+
// across calls. The old `nwss-ovpn${activeConnections.size}` used the
|
|
189
|
+
// live Map size, so disconnect computed a DIFFERENT name than connect
|
|
190
|
+
// did (size had grown in between) and silently failed to find the
|
|
191
|
+
// entry — the connection would leak until disconnectAll. Same fix as
|
|
192
|
+
// wireguard_vpn.js commit 478a3ad.
|
|
193
|
+
if (vpnConfig.config_inline) {
|
|
194
|
+
const hash = crypto.createHash('sha1').update(vpnConfig.config_inline).digest('hex').slice(0, 8);
|
|
195
|
+
return `nwss-ovpn${hash}`;
|
|
196
|
+
}
|
|
197
|
+
// Last resort — should be unreachable if validation ran first.
|
|
198
|
+
return 'nwss-ovpn-unknown';
|
|
174
199
|
}
|
|
175
200
|
|
|
176
201
|
/**
|
|
@@ -359,14 +384,23 @@ async function startConnection(configPath, vpnConfig, forceDebug = false) {
|
|
|
359
384
|
return { success: true, connection: connectionName, tunDevice: existing.tunDevice, alreadyActive: true };
|
|
360
385
|
}
|
|
361
386
|
|
|
362
|
-
// Kill any stale processes from a previous run using this config
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
387
|
+
// Kill any stale processes from a previous run using this config.
|
|
388
|
+
// spawnSync with arg array (no shell) — connectionName flows from
|
|
389
|
+
// user config (vpnConfig.name). Naive shell interpolation here was
|
|
390
|
+
// vulnerable to '`; rm -rf ~; #' style injection.
|
|
391
|
+
const pkillRes = spawnSync('sudo', ['pkill', '-TERM', '-f', `openvpn.*${connectionName}`], {
|
|
392
|
+
encoding: 'utf8', timeout: 3000
|
|
393
|
+
});
|
|
394
|
+
// Only sleep if pkill actually matched + killed something (status 0).
|
|
395
|
+
// pkill exits 1 when no processes match — pre-execSync code threw on
|
|
396
|
+
// non-zero and the surrounding try/catch swallowed it, so the sleep
|
|
397
|
+
// never ran in the no-stale-process case. With spawnSync (no throw
|
|
398
|
+
// on non-zero), we have to gate this explicitly to preserve the same
|
|
399
|
+
// behavior — otherwise every fresh connect would waste 1s of
|
|
400
|
+
// blocking event loop.
|
|
401
|
+
if (pkillRes.status === 0) {
|
|
402
|
+
spawnSync('sleep', ['1'], { timeout: 3000 });
|
|
403
|
+
}
|
|
370
404
|
|
|
371
405
|
ensureTempDir();
|
|
372
406
|
const logPath = path.join(TEMP_DIR, `${connectionName}.log`);
|
|
@@ -411,7 +445,17 @@ async function startConnection(configPath, vpnConfig, forceDebug = false) {
|
|
|
411
445
|
if (!result.connected) {
|
|
412
446
|
// Kill the process if still running
|
|
413
447
|
try { child.kill('SIGTERM'); } catch {}
|
|
414
|
-
|
|
448
|
+
// C2: check exitCode/signalCode before SIGKILL — child.kill uses the
|
|
449
|
+
// captured PID, and Linux PIDs can be reused. If the process already
|
|
450
|
+
// exited in the 3s window, a reused PID could belong to an unrelated
|
|
451
|
+
// process that we'd then SIGKILL. unref() lets the event loop exit
|
|
452
|
+
// naturally instead of waiting on this background cleanup timer.
|
|
453
|
+
const sigkillTimer = setTimeout(() => {
|
|
454
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
455
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
456
|
+
}
|
|
457
|
+
}, 3000);
|
|
458
|
+
if (typeof sigkillTimer.unref === 'function') sigkillTimer.unref();
|
|
415
459
|
return { success: false, connection: connectionName, error: result.error };
|
|
416
460
|
}
|
|
417
461
|
|
|
@@ -441,21 +485,19 @@ function stopConnection(connectionName, forceDebug = false) {
|
|
|
441
485
|
}
|
|
442
486
|
|
|
443
487
|
try {
|
|
444
|
-
// Find the actual openvpn PID (child of sudo) and kill it
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
488
|
+
// Find the actual openvpn PID (child of sudo) and kill it.
|
|
489
|
+
// spawnSync with arg array — connectionName from user config, naive
|
|
490
|
+
// shell interpolation was vulnerable to ';rm -rf ~' style injection.
|
|
491
|
+
spawnSync('sudo', ['pkill', '-TERM', '-f', `openvpn.*${connectionName}`], {
|
|
492
|
+
encoding: 'utf8', timeout: 3000
|
|
493
|
+
});
|
|
450
494
|
|
|
451
495
|
const killed = waitForProcessExit(info.pid, 5000);
|
|
452
496
|
if (!killed) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
} catch {}
|
|
458
|
-
}
|
|
497
|
+
spawnSync('sudo', ['pkill', '-9', '-f', `openvpn.*${connectionName}`], {
|
|
498
|
+
encoding: 'utf8', timeout: 3000
|
|
499
|
+
});
|
|
500
|
+
}
|
|
459
501
|
} catch (killErr) {
|
|
460
502
|
// Process may already be dead
|
|
461
503
|
if (forceDebug) {
|
|
@@ -532,13 +574,18 @@ function checkConnection(connectionName, testHost = '1.1.1.1', forceDebug = fals
|
|
|
532
574
|
return { connected: false, error: `OpenVPN process exited with code ${info.process.exitCode}` };
|
|
533
575
|
}
|
|
534
576
|
|
|
535
|
-
// Ping through the tunnel interface
|
|
577
|
+
// Ping through the tunnel interface. spawnSync with arg array —
|
|
578
|
+
// testHost flows from user config (vpnConfig.test_host), naive shell
|
|
579
|
+
// interpolation was vulnerable to '1.1.1.1; rm -rf ~' style injection.
|
|
536
580
|
try {
|
|
537
581
|
const iface = info.tunDevice || 'tun0';
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
)
|
|
582
|
+
const pingRes = spawnSync('ping', ['-c', '1', '-W', '5', '-I', iface, testHost], {
|
|
583
|
+
encoding: 'utf8', timeout: 8000
|
|
584
|
+
});
|
|
585
|
+
if (pingRes.status !== 0) {
|
|
586
|
+
throw new Error((pingRes.stderr || pingRes.stdout || '').split('\n')[0] || `ping failed for ${testHost}`);
|
|
587
|
+
}
|
|
588
|
+
const result = pingRes.stdout;
|
|
542
589
|
|
|
543
590
|
const latencyMatch = result.match(/time=([0-9.]+)\s*ms/);
|
|
544
591
|
const latencyMs = latencyMatch ? parseFloat(latencyMatch[1]) : null;
|
|
@@ -660,6 +707,23 @@ function validateOvpnConfig(ovpnConfig) {
|
|
|
660
707
|
result.warnings.push('Both "config" and "config_inline" provided; "config" takes precedence');
|
|
661
708
|
}
|
|
662
709
|
|
|
710
|
+
// D1: Validate user-provided connection 'name' to prevent path traversal
|
|
711
|
+
// via writeAuthFile/writeInlineConfig's path.join, AND to limit the blast
|
|
712
|
+
// radius of any future shell-interpolation that re-introduces ${name}.
|
|
713
|
+
// The current shell calls all use spawnSync with arg arrays (S1 fix), but
|
|
714
|
+
// the regex still blocks values that would confuse pkill's -f pattern
|
|
715
|
+
// match. Length 32 = generous for connection names but bounded.
|
|
716
|
+
// Mirror of wireguard_vpn.js F1 validation.
|
|
717
|
+
if (ovpnConfig.name !== undefined && ovpnConfig.name !== null) {
|
|
718
|
+
if (typeof ovpnConfig.name !== 'string' || !/^[a-zA-Z0-9_-]{1,32}$/.test(ovpnConfig.name)) {
|
|
719
|
+
result.isValid = false;
|
|
720
|
+
result.errors.push(
|
|
721
|
+
`Invalid 'name' value ${JSON.stringify(ovpnConfig.name)}: ` +
|
|
722
|
+
`must match /^[a-zA-Z0-9_-]{1,32}$/ (path-safe chars, max 32)`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
663
727
|
// Validate config file exists
|
|
664
728
|
if (ovpnConfig.config) {
|
|
665
729
|
const configPath = ovpnConfig.config;
|
|
@@ -796,7 +860,24 @@ async function connectForSite(siteConfig, forceDebug = false) {
|
|
|
796
860
|
}
|
|
797
861
|
}
|
|
798
862
|
|
|
799
|
-
|
|
863
|
+
// C3: Only fetch external IP when debug-logging would actually use it.
|
|
864
|
+
// getExternalIP runs up to 3 sequential 8s-timeout curls (~24s worst
|
|
865
|
+
// case of blocking event loop) per VPN connect. Without this gate,
|
|
866
|
+
// every successful OpenVPN connect burned 1-24s on a value that's
|
|
867
|
+
// only displayed in the nwss info log when present — and only
|
|
868
|
+
// genuinely useful for debugging. Matches wireguard_vpn.js's
|
|
869
|
+
// forceDebug-gated approach (commit b97dedb).
|
|
870
|
+
//
|
|
871
|
+
// UX trade-off: the nwss info log (nwss.js:2273) only shows the IP
|
|
872
|
+
// when forceDebug is on. Users wanting the IP in non-debug runs need
|
|
873
|
+
// --debug; same behavior as WireGuard.
|
|
874
|
+
let externalIP = null;
|
|
875
|
+
if (forceDebug) {
|
|
876
|
+
externalIP = getExternalIP(startResult.tunDevice);
|
|
877
|
+
if (externalIP) {
|
|
878
|
+
console.log(formatLogMessage('debug', `${OPENVPN_TAG} ${connectionName} external IP: ${externalIP}`));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
800
881
|
return { success: true, connection: connectionName, tunDevice: startResult.tunDevice, externalIP };
|
|
801
882
|
}
|
|
802
883
|
|
package/lib/proxy.js
CHANGED
|
@@ -253,8 +253,12 @@ function getProxyArgs(siteConfig, forceDebug = false) {
|
|
|
253
253
|
console.warn(formatLogMessage('proxy', `proxy_remote_dns ignored: SOCKS4 cannot do proxy-side DNS resolution (use SOCKS5)`));
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// Bypass list: domains that skip the proxy
|
|
257
|
-
|
|
256
|
+
// Bypass list: domains that skip the proxy. Accept either an array (the
|
|
257
|
+
// documented form) or a single string — a bare "localhost" used to throw
|
|
258
|
+
// `bypass.join is not a function` here, in the browser-launch path. Same
|
|
259
|
+
// string-or-array tolerance as the dig/whois siteConfig fields.
|
|
260
|
+
const rawBypass = siteConfig.proxy_bypass || siteConfig.socks5_bypass || [];
|
|
261
|
+
const bypass = Array.isArray(rawBypass) ? rawBypass : [rawBypass];
|
|
258
262
|
if (bypass.length > 0) {
|
|
259
263
|
args.push(`--proxy-bypass-list=${bypass.join(';')}`);
|
|
260
264
|
}
|