@fanboynz/network-scanner 2.0.44 → 2.0.46

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/README.md CHANGED
@@ -152,8 +152,9 @@ Example:
152
152
  | `even_blocked` | Boolean | `false` | Add matching rules even if requests are blocked |
153
153
  | `bypass_cache` | Boolean | `false` | Skip all caching for this site's URLs |
154
154
  | `window_cleanup` | Boolean or String | `false` | Close old/unused browser windows/tabs after entire URL group completes |
155
+ | `window_cleanup_threshold` | Integer | `8` | For `"realtime"` mode: max pages to keep open before cleanup |
155
156
 
156
- **Window cleanup modes:** `false` (disabled), `true` (conservative - closes obvious leftovers), `"all"` (aggressive - closes all content pages). Both active modes preserve the main Puppeteer window and wait 16 seconds before cleanup to avoid interfering with active operations.
157
+ **Window cleanup modes:** `false` (disabled), `true` (conservative - closes obvious leftovers), `"realtime"` (continuously cleanup oldest pages when threshold exceeded), `"all"` (aggressive - closes all content pages after group). Both active modes preserve the main Puppeteer window and wait 16 seconds before cleanup to avoid interfering with active operations.
157
158
 
158
159
 
159
160
  ### Redirect Handling Options
@@ -193,6 +194,11 @@ When a page redirects to a new domain, first-party/third-party detection is base
193
194
  "referrer_headers": {"mode": "custom", "custom": ["https://news.ycombinator.com/"]}
194
195
  ```
195
196
 
197
+ **Disable referrer for specific URLs:**
198
+ ```json
199
+ "referrer_disable": ["https://example.com/no-ref", "sensitive-site.com"]
200
+ ```
201
+
196
202
  ### Protection Bypassing
197
203
 
198
204
  | Field | Values | Default | Description |
@@ -244,7 +250,7 @@ When a page redirects to a new domain, first-party/third-party detection is base
244
250
  |:---------------------|:-------|:-------:|:------------|
245
251
  | `goto_options` | Object | `{"waitUntil": "load"}` | Custom page.goto() options |
246
252
  | `clear_sitedata` | Boolean | `false` | Clear all cookies, cache, storage before each load |
247
- | `forcereload` | Boolean | `false` | Force an additional reload after reloads |
253
+ | `forcereload` | Boolean or Array | `false` | Force cache-clearing reload for all URLs (`true`) or specific domains (`["domain1.com"]`) |
248
254
  | `isBrave` | Boolean | `false` | Spoof Brave browser detection |
249
255
  | `evaluateOnNewDocument` | Boolean | `false` | Inject fetch/XHR interceptor in page |
250
256
  | `cdp` | Boolean | `false` | Enable CDP logging for this site |
@@ -260,6 +266,54 @@ When a page redirects to a new domain, first-party/third-party detection is base
260
266
  | `interact_clicks` | Boolean | `false` | Enable element clicking simulation |
261
267
  | `interact_typing` | Boolean | `false` | Enable typing simulation |
262
268
  | `interact_intensity` | String | `"medium"` | Interaction simulation intensity: "low", "medium", "high" |
269
+ | `dnsmasq` | Boolean | `false` | Force dnsmasq output for this site |
270
+ | `dnsmasq_old` | Boolean | `false` | Force dnsmasq old format output for this site |
271
+ | `unbound` | Boolean | `false` | Force unbound output for this site |
272
+ | `privoxy` | Boolean | `false` | Force Privoxy output for this site |
273
+ | `pihole` | Boolean | `false` | Force Pi-hole regex output for this site |
274
+ | `ignore_similar` | Boolean | - | Override global `ignore_similar` setting for this site |
275
+ | `ignore_similar_threshold` | Integer | - | Override global similarity threshold for this site |
276
+ | `ignore_similar_ignored_domains` | Boolean | - | Override global `ignore_similar_ignored_domains` for this site |
277
+
278
+ ### VPN Options
279
+
280
+ Route traffic through a VPN for specific sites. Requires `sudo` privileges. The VPN connection is established before scanning and torn down after the site completes.
281
+
282
+ > **Note:** VPN modifies system-level routing. During concurrent scanning, all traffic routes through the active tunnel — not just the site that requested it. For isolated per-site VPN, run sites sequentially or use the same VPN config for all concurrent sites.
283
+
284
+ #### WireGuard
285
+
286
+ | Field | Values | Default | Description |
287
+ |:---------------------|:-------|:-------:|:------------|
288
+ | `vpn` | String or Object | - | WireGuard VPN configuration |
289
+ | `vpn.config` | String | - | Path to `.conf` file or interface name in `/etc/wireguard/` |
290
+ | `vpn.config_inline` | String | - | Inline WireGuard config content |
291
+ | `vpn.interface` | String | auto | Interface name (auto-derived from config filename) |
292
+ | `vpn.health_check` | Boolean | `true` | Ping through tunnel to verify connectivity |
293
+ | `vpn.test_host` | String | `"1.1.1.1"` | Host to ping for health check |
294
+ | `vpn.retry` | Boolean | `true` | Retry on connection failure |
295
+ | `vpn.max_retries` | Integer | `2` | Maximum retry attempts |
296
+
297
+ #### OpenVPN
298
+
299
+ | Field | Values | Default | Description |
300
+ |:---------------------|:-------|:-------:|:------------|
301
+ | `openvpn` | String or Object | - | OpenVPN configuration |
302
+ | `openvpn.config` | String | - | Path to `.ovpn` file |
303
+ | `openvpn.config_inline` | String | - | Inline OpenVPN config content |
304
+ | `openvpn.name` | String | auto | Connection name (auto-derived from config filename) |
305
+ | `openvpn.username` | String | - | VPN username (written to secure temp file) |
306
+ | `openvpn.password` | String | - | VPN password (written to secure temp file) |
307
+ | `openvpn.auth_file` | String | - | Path to existing auth credentials file |
308
+ | `openvpn.health_check` | Boolean | `true` | Ping through tunnel to verify connectivity |
309
+ | `openvpn.test_host` | String | `"1.1.1.1"` | Host to ping for health check |
310
+ | `openvpn.retry` | Boolean | `true` | Retry on connection failure |
311
+ | `openvpn.max_retries` | Integer | `2` | Maximum retry attempts |
312
+ | `openvpn.connect_timeout` | Milliseconds | `30000` | Timeout for connection establishment |
313
+ | `openvpn.extra_args` | Array | - | Additional OpenVPN command line arguments |
314
+ | `openvpn.verbosity` | String | `"3"` | OpenVPN log verbosity level |
315
+
316
+ > **Authentication:** If the `.ovpn` file already contains credentials (via `auth-user-pass /path/to/file` or an inline `<auth-user-pass>` block), no additional config is needed — just provide the config path. The `username`/`password` fields are only needed when the `.ovpn` file has a bare `auth-user-pass` directive that expects interactive input.
263
317
 
264
318
  ### Global Configuration Options
265
319
 
@@ -434,6 +488,41 @@ node nwss.js --max-concurrent 12 --cleanup-interval 300 -o rules.txt
434
488
  }
435
489
  ```
436
490
 
491
+ #### Scanning through OpenVPN
492
+ ```json
493
+ {
494
+ "url": "https://geo-restricted-site.com",
495
+ "openvpn": "/etc/openvpn/us-server.ovpn",
496
+ "filterRegex": "tracking|analytics",
497
+ "userAgent": "chrome",
498
+ "fingerprint_protection": "random"
499
+ }
500
+ ```
501
+
502
+ #### OpenVPN with Credentials
503
+ ```json
504
+ {
505
+ "url": "https://region-locked-site.com",
506
+ "openvpn": {
507
+ "config": "/etc/openvpn/eu-server.ovpn",
508
+ "username": "vpn_user",
509
+ "password": "vpn_pass",
510
+ "connect_timeout": 45000
511
+ },
512
+ "filterRegex": "ads|tracking"
513
+ }
514
+ ```
515
+
516
+ #### Scanning through WireGuard
517
+ ```json
518
+ {
519
+ "url": "https://another-site.com",
520
+ "vpn": "/etc/wireguard/wg-us.conf",
521
+ "filterRegex": "analytics",
522
+ "userAgent": "firefox"
523
+ }
524
+ ```
525
+
437
526
  ---
438
527
 
439
528
  ## Memory Management
@@ -477,6 +566,39 @@ sudo apt install google-chrome-stable
477
566
  sudo apt install bind9-dnsutils whois
478
567
  ```
479
568
 
569
+ #### OpenVPN (optional, for per-site VPN routing)
570
+ ```
571
+ sudo apt install openvpn
572
+ ```
573
+
574
+ Grant passwordless sudo for OpenVPN operations:
575
+ ```
576
+ sudo visudo -f /etc/sudoers.d/openvpn-nwss
577
+ ```
578
+ Add:
579
+ ```
580
+ your_username ALL=(root) NOPASSWD: /usr/sbin/openvpn, /usr/bin/kill, /usr/bin/pgrep, /usr/bin/pkill
581
+ ```
582
+
583
+ On WSL2, load the TUN module (required each reboot):
584
+ ```
585
+ sudo modprobe tun
586
+ ```
587
+
588
+ #### WireGuard (optional, for per-site VPN routing)
589
+ ```
590
+ sudo apt install wireguard
591
+ ```
592
+
593
+ Grant passwordless sudo for WireGuard operations:
594
+ ```
595
+ sudo visudo -f /etc/sudoers.d/wg-nwss
596
+ ```
597
+ Add:
598
+ ```
599
+ your_username ALL=(root) NOPASSWD: /usr/bin/wg-quick, /usr/bin/wg
600
+ ```
601
+
480
602
  ## Notes
481
603
 
482
604
  - If both `firstParty: 0` and `thirdParty: 0` are set for a site, it will be skipped.
@@ -491,5 +613,9 @@ sudo apt install bind9-dnsutils whois
491
613
  - For maximum stealth, combine `fingerprint_protection: "random"` with appropriate `referrer_headers` modes
492
614
  - User agents are automatically updated to latest versions (Chrome 131, Firefox 133, Safari 18.2)
493
615
  - Referrer headers work independently from fingerprint protection - use both for best results
616
+ - VPN connections (`vpn`/`openvpn`) are established before scanning and torn down after the site completes
617
+ - If an `.ovpn` file contains embedded credentials, no additional auth config is needed in the JSON
618
+ - VPN affects system-level routing — all concurrent scans will route through the active tunnel
619
+ - Both `vpn` (WireGuard) and `openvpn` can be set, but `vpn` takes precedence
494
620
 
495
621
  ---
package/lib/cdp.js CHANGED
@@ -425,7 +425,5 @@ async function createEnhancedCDPSession(page, currentUrl, options = {}) {
425
425
  module.exports = {
426
426
  createCDPSession,
427
427
  createPageWithTimeout,
428
- setRequestInterceptionWithTimeout,
429
- validateCDPConfig,
430
- createEnhancedCDPSession
428
+ setRequestInterceptionWithTimeout
431
429
  };
@@ -345,6 +345,5 @@ async function clearSiteDataEnhanced(page, currentUrl, forceDebug) {
345
345
  module.exports = {
346
346
  clearSiteData,
347
347
  clearSiteDataViaCDP,
348
- clearSiteDataViaPage,
349
- clearSiteDataEnhanced
348
+ clearSiteDataViaPage
350
349
  };
@@ -373,7 +373,6 @@ async function humanLikeMouseMove(page, fromX, fromY, toX, toY, options = {}) {
373
373
 
374
374
  // Add slight curve to movement (more human-like)
375
375
  if (curve > 0 && i > 0 && i < actualSteps) {
376
- const midpoint = actualSteps / 2;
377
376
  const curveIntensity = Math.sin((i / actualSteps) * Math.PI) * curve * distance * MOUSE_MOVEMENT.CURVE_INTENSITY_RATIO;
378
377
  const perpX = -(toY - fromY) / distance;
379
378
  const perpY = (toX - fromX) / distance;
@@ -449,7 +448,7 @@ async function simulateScrolling(page, options = {}) {
449
448
  // Smooth scrolling by breaking into smaller increments
450
449
  for (let j = 0; j < smoothness; j++) {
451
450
  await page.mouse.wheel({ deltaY: scrollDelta / smoothness });
452
- await new Promise(resolve => setTimeout(resolve, SCROLLING.SMOOTH_INCREMENT_DELAY));
451
+ await fastTimeout(SCROLLING.SMOOTH_INCREMENT_DELAY);
453
452
  }
454
453
 
455
454
  if (i < amount - 1) {
@@ -549,7 +548,7 @@ async function interactWithElements(page, options = {}) {
549
548
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
550
549
  try {
551
550
  // Find visible, clickable elements
552
- const elements = await page.evaluate((selectors, avoidWords) => {
551
+ const elements = await page.evaluate((selectors, avoidWords, textPreviewLen) => {
553
552
  const clickableElements = [];
554
553
 
555
554
  selectors.forEach(selector => {
@@ -571,7 +570,7 @@ async function interactWithElements(page, options = {}) {
571
570
  y: rect.top + rect.height / 2,
572
571
  width: rect.width,
573
572
  height: rect.height,
574
- text: text.substring(0, ELEMENT_INTERACTION.TEXT_PREVIEW_LENGTH)
573
+ text: text.substring(0, textPreviewLen)
575
574
  });
576
575
  }
577
576
  }
@@ -579,7 +578,7 @@ async function interactWithElements(page, options = {}) {
579
578
  });
580
579
 
581
580
  return clickableElements;
582
- }, elementTypes, avoidDestructive ? ['delete', 'remove', 'submit', 'buy', 'purchase', 'order'] : []);
581
+ }, elementTypes, avoidDestructive ? ['delete', 'remove', 'submit', 'buy', 'purchase', 'order'] : [], ELEMENT_INTERACTION.TEXT_PREVIEW_LENGTH);
583
582
 
584
583
  if (elements.length > 0) {
585
584
  // Choose a random element to interact with
@@ -590,12 +589,12 @@ async function interactWithElements(page, options = {}) {
590
589
  await humanLikeMouseMove(page, currentPos.x, currentPos.y, element.x, element.y);
591
590
 
592
591
  // Brief pause before clicking
593
- await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * TIMING.CLICK_PAUSE_MAX);
592
+ await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * (TIMING.CLICK_PAUSE_MAX - TIMING.CLICK_PAUSE_MIN));
594
593
 
595
594
  await page.mouse.click(element.x, element.y);
596
595
 
597
596
  // Brief pause after clicking
598
- await fastTimeout(TIMING.POST_CLICK_MIN + Math.random() * TIMING.POST_CLICK_MAX);
597
+ await fastTimeout(TIMING.POST_CLICK_MIN + Math.random() * (TIMING.POST_CLICK_MAX - TIMING.POST_CLICK_MIN));
599
598
  }
600
599
  } catch (elementErr) {
601
600
  // Continue to next attempt if this one fails
@@ -673,9 +672,9 @@ async function simulateTyping(page, text, options = {}) {
673
672
  if (mistakes && Math.random() < mistakeRate) {
674
673
  const wrongChar = String.fromCharCode(97 + Math.floor(Math.random() * 26));
675
674
  await page.keyboard.type(wrongChar);
676
- await fastTimeout(TIMING.MISTAKE_PAUSE_MIN + Math.random() * TIMING.MISTAKE_PAUSE_MAX);
675
+ await fastTimeout(TIMING.MISTAKE_PAUSE_MIN + Math.random() * (TIMING.MISTAKE_PAUSE_MAX - TIMING.MISTAKE_PAUSE_MIN));
677
676
  await page.keyboard.press('Backspace');
678
- await fastTimeout(TIMING.BACKSPACE_DELAY_MIN + Math.random() * TIMING.BACKSPACE_DELAY_MAX);
677
+ await fastTimeout(TIMING.BACKSPACE_DELAY_MIN + Math.random() * (TIMING.BACKSPACE_DELAY_MAX - TIMING.BACKSPACE_DELAY_MIN));
679
678
  }
680
679
 
681
680
  await page.keyboard.type(char);
@@ -818,6 +817,7 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
818
817
  }
819
818
  return;
820
819
  }
820
+ await bodyExists.dispose();
821
821
  } catch (bodyCheckErr) {
822
822
  if (forceDebug) {
823
823
  console.log(`[interaction] Page not ready for interaction on ${currentUrl} (waited ${Math.min(Math.max((options.siteTimeout || 20000) / 8, 2000), 5000)}ms): ${bodyCheckErr.message}`);
@@ -922,6 +922,7 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
922
922
  const bodyElement = await page.$('body');
923
923
  if (bodyElement) {
924
924
  await page.hover('body');
925
+ await bodyElement.dispose();
925
926
  }
926
927
  } catch (hoverErr) {
927
928
  // Silently handle hover failures - not critical
@@ -13,6 +13,22 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { formatLogMessage } = require('./colorize');
15
15
 
16
+ /**
17
+ * Fetch external IP address through the active tunnel
18
+ * @param {string} tunDevice - TUN device name (optional)
19
+ * @returns {string|null} External IP or null
20
+ */
21
+ function getExternalIP(tunDevice) {
22
+ const services = ['https://api.ipify.org', 'https://ifconfig.me/ip', 'https://icanhazip.com'];
23
+ for (const service of services) {
24
+ try {
25
+ const iface = tunDevice ? `--interface ${tunDevice}` : '';
26
+ return execSync(`curl -s -m 5 ${iface} ${service}`, { encoding: 'utf8', timeout: 8000 }).trim();
27
+ } catch {}
28
+ }
29
+ return null;
30
+ }
31
+
16
32
  // Track active connections: name ? { process, configPath, pid, tunDevice, startedAt, sites }
17
33
  const activeConnections = new Map();
18
34
 
@@ -107,7 +123,7 @@ function checkTunDevice() {
107
123
  */
108
124
  function ensureTempDir() {
109
125
  if (!fs.existsSync(TEMP_DIR)) {
110
- fs.mkdirSync(TEMP_DIR, { recursive: true, mode: 0o700 });
126
+ fs.mkdirSync(TEMP_DIR, { recursive: true, mode: 0o755 });
111
127
  }
112
128
  }
113
129
 
@@ -340,12 +356,24 @@ async function startConnection(configPath, vpnConfig, forceDebug = false) {
340
356
  return { success: true, connection: connectionName, tunDevice: existing.tunDevice, alreadyActive: true };
341
357
  }
342
358
 
359
+ // Kill any stale processes from a previous run using this config
360
+ try {
361
+ execSync(`sudo pkill -TERM -f "openvpn.*${connectionName}" 2>/dev/null`, {
362
+ encoding: 'utf8', timeout: 3000
363
+ });
364
+ // Brief wait for cleanup
365
+ execSync('sleep 1', { timeout: 3000 });
366
+ } catch {}
367
+
343
368
  ensureTempDir();
344
369
  const logPath = path.join(TEMP_DIR, `${connectionName}.log`);
345
370
 
346
371
  // Clean stale log
347
372
  try { if (fs.existsSync(logPath)) fs.unlinkSync(logPath); } catch {}
348
373
 
374
+ // Pre-create log file writable by all so sudo openvpn can write and user can read
375
+ try { fs.writeFileSync(logPath, '', { mode: 0o666 }); } catch {}
376
+
349
377
  const args = buildArgs(configPath, vpnConfig, connectionName, logPath);
350
378
 
351
379
  if (forceDebug) {
@@ -365,7 +393,7 @@ async function startConnection(configPath, vpnConfig, forceDebug = false) {
365
393
  fgArgs.push(args[i]);
366
394
  }
367
395
 
368
- const child = spawn('openvpn', fgArgs, {
396
+ const child = spawn('sudo', ['openvpn', ...fgArgs], {
369
397
  stdio: ['ignore', 'ignore', 'ignore'],
370
398
  detached: false
371
399
  });
@@ -411,17 +439,21 @@ function stopConnection(connectionName, forceDebug = false) {
411
439
  }
412
440
 
413
441
  try {
414
- // Send SIGTERM to gracefully disconnect
415
- if (info.process && info.process.exitCode === null) {
416
- info.process.kill('SIGTERM');
417
-
418
- // Wait briefly for graceful shutdown, then force kill
419
- const killed = waitForProcessExit(info.pid, 5000);
420
- if (!killed) {
421
- try { info.process.kill('SIGKILL'); } catch {}
422
- try { process.kill(info.pid, 'SIGKILL'); } catch {}
442
+ // Find the actual openvpn PID (child of sudo) and kill it
443
+ try {
444
+ execSync(`sudo pkill -TERM -f "openvpn.*${connectionName}" 2>/dev/null`, {
445
+ encoding: 'utf8', timeout: 3000
446
+ });
447
+ } catch {}
448
+
449
+ const killed = waitForProcessExit(info.pid, 5000);
450
+ if (!killed) {
451
+ try {
452
+ execSync(`sudo pkill -9 -f "openvpn.*${connectionName}" 2>/dev/null`, {
453
+ encoding: 'utf8', timeout: 3000
454
+ });
455
+ } catch {}
423
456
  }
424
- }
425
457
  } catch (killErr) {
426
458
  // Process may already be dead
427
459
  if (forceDebug) {
@@ -766,7 +798,8 @@ async function connectForSite(siteConfig, forceDebug = false) {
766
798
  }
767
799
  }
768
800
 
769
- return { success: true, connection: connectionName, tunDevice: startResult.tunDevice };
801
+ const externalIP = getExternalIP(startResult.tunDevice);
802
+ return { success: true, connection: connectionName, tunDevice: startResult.tunDevice, externalIP };
770
803
  }
771
804
 
772
805
  return { success: false, connection: connectionName, error: 'All attempts failed' };
@@ -7,6 +7,22 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { formatLogMessage } = require('./colorize');
9
9
 
10
+ /**
11
+ * Fetch external IP address through the active tunnel
12
+ * @param {string} interfaceName - WireGuard interface name (optional)
13
+ * @returns {string|null} External IP or null
14
+ */
15
+ function getExternalIP(interfaceName) {
16
+ const services = ['https://api.ipify.org', 'https://ifconfig.me/ip', 'https://icanhazip.com'];
17
+ for (const service of services) {
18
+ try {
19
+ const iface = interfaceName ? `--interface ${interfaceName}` : '';
20
+ return execSync(`curl -s -m 5 ${iface} ${service}`, { encoding: 'utf8', timeout: 8000 }).trim();
21
+ } catch {}
22
+ }
23
+ return null;
24
+ }
25
+
10
26
  // Track active interfaces for cleanup
11
27
  const activeInterfaces = new Map();
12
28
 
@@ -94,7 +110,7 @@ function interfaceUp(configPath, interfaceName, forceDebug = false) {
94
110
 
95
111
  try {
96
112
  // wg-quick accepts a config path or interface name in /etc/wireguard/
97
- execSync(`wg-quick up "${configPath}"`, {
113
+ execSync(`sudo wg-quick up "${configPath}"`, {
98
114
  encoding: 'utf8',
99
115
  timeout: 15000
100
116
  });
@@ -109,7 +125,8 @@ function interfaceUp(configPath, interfaceName, forceDebug = false) {
109
125
  console.log(formatLogMessage('debug', `[vpn] Interface ${interfaceName} is up`));
110
126
  }
111
127
 
112
- return { success: true, interface: interfaceName };
128
+ const externalIP = getExternalIP(interfaceName);
129
+ return { success: true, interface: interfaceName, externalIP };
113
130
  } catch (error) {
114
131
  return {
115
132
  success: false,
@@ -132,7 +149,7 @@ function interfaceDown(interfaceName, forceDebug = false) {
132
149
  }
133
150
 
134
151
  try {
135
- execSync(`wg-quick down "${info.configPath}"`, {
152
+ execSync(`sudo wg-quick down "${info.configPath}"`, {
136
153
  encoding: 'utf8',
137
154
  timeout: 10000
138
155
  });
package/nwss.js CHANGED
@@ -1673,7 +1673,8 @@ function setupFrameHandling(page, forceDebug) {
1673
1673
  return { url: currentUrl, rules: [], success: false, vpnFailed: true };
1674
1674
  }
1675
1675
  if (!silentMode) {
1676
- console.log(formatLogMessage('info', `[vpn] WireGuard connected via ${vpnResult.interface} for ${currentUrl}`));
1676
+ const ipInfo = vpnResult.externalIP ? ` (${vpnResult.externalIP})` : '';
1677
+ console.log(formatLogMessage('info', `[vpn] WireGuard connected via ${vpnResult.interface}${ipInfo} for ${currentUrl}`));
1677
1678
  }
1678
1679
  } else if (siteConfig.openvpn) {
1679
1680
  const ovpnResult = await ovpnConnect(siteConfig, forceDebug);
@@ -1682,7 +1683,8 @@ function setupFrameHandling(page, forceDebug) {
1682
1683
  return { url: currentUrl, rules: [], success: false, vpnFailed: true };
1683
1684
  }
1684
1685
  if (!silentMode) {
1685
- console.log(formatLogMessage('info', `[vpn] OpenVPN connected via ${ovpnResult.connection} for ${currentUrl}`));
1686
+ const ipInfo = ovpnResult.externalIP ? ` (${ovpnResult.externalIP})` : '';
1687
+ console.log(formatLogMessage('info', `[vpn] OpenVPN connected via ${ovpnResult.connection}${ipInfo} for ${currentUrl}`));
1686
1688
  }
1687
1689
  }
1688
1690
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.44",
3
+ "version": "2.0.46",
4
4
  "description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
5
5
  "main": "nwss.js",
6
6
  "scripts": {