@fanboynz/network-scanner
Version:
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.
526 lines (456 loc) • 15.8 kB
JavaScript
// === WireGuard VPN Module ===
// Per-site VPN configuration for network scanner
// Manages WireGuard interfaces, routing, and lifecycle
const { execSync, exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const { formatLogMessage } = require('./colorize');
/**
* Fetch external IP address through the active tunnel
* @param {string} interfaceName - WireGuard interface name (optional)
* @returns {string|null} External IP or null
*/
function getExternalIP(interfaceName) {
const services = ['https://api.ipify.org', 'https://ifconfig.me/ip', 'https://icanhazip.com'];
for (const service of services) {
try {
const iface = interfaceName ? `--interface ${interfaceName}` : '';
return execSync(`curl -s -m 5 ${iface} ${service}`, { encoding: 'utf8', timeout: 8000 }).trim();
} catch {}
}
return null;
}
// Track active interfaces for cleanup
const activeInterfaces = new Map();
// Temp config directory for inline configs
const TEMP_CONFIG_DIR = '/tmp/nwss-wireguard';
/**
* Validate WireGuard availability on the system
* @returns {Object} { isAvailable, version, error }
*/
function validateWireGuardAvailability() {
try {
const version = execSync('wg --version 2>&1', { encoding: 'utf8' }).trim();
// Check wg-quick is also available
execSync('which wg-quick', { encoding: 'utf8' });
return { isAvailable: true, version };
} catch (error) {
return {
isAvailable: false,
error: 'WireGuard not found. Install with: sudo apt install wireguard'
};
}
}
/**
* Check if running with sufficient privileges
* @returns {boolean}
*/
function hasRootPrivileges() {
try {
return process.getuid() === 0;
} catch {
return false;
}
}
/**
* Resolve interface name from config path or explicit name
* @param {Object} vpnConfig - VPN configuration object
* @returns {string} Interface name
*/
function resolveInterfaceName(vpnConfig) {
if (vpnConfig.interface) {
return vpnConfig.interface;
}
if (vpnConfig.config) {
// Extract name from /etc/wireguard/wg-example.conf ? wg-example
return path.basename(vpnConfig.config, '.conf');
}
// Auto-generate from index
const index = activeInterfaces.size;
return `wg-nwss${index}`;
}
/**
* Write inline config to temp file
* @param {string} interfaceName - Interface name for the file
* @param {string} configContent - WireGuard config content
* @returns {string} Path to temp config file
*/
function writeInlineConfig(interfaceName, configContent) {
if (!fs.existsSync(TEMP_CONFIG_DIR)) {
fs.mkdirSync(TEMP_CONFIG_DIR, { recursive: true, mode: 0o700 });
}
const configPath = path.join(TEMP_CONFIG_DIR, `${interfaceName}.conf`);
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
return configPath;
}
/**
* Bring up a WireGuard interface
* @param {string} configPath - Path to .conf file (without extension for wg-quick)
* @param {string} interfaceName - Interface name
* @param {boolean} forceDebug - Debug logging
* @returns {Object} { success, interface, error }
*/
function interfaceUp(configPath, interfaceName, forceDebug = false) {
if (activeInterfaces.has(interfaceName)) {
if (forceDebug) {
console.log(formatLogMessage('debug', `[vpn] Interface ${interfaceName} already active`));
}
return { success: true, interface: interfaceName, alreadyActive: true };
}
try {
// wg-quick accepts a config path or interface name in /etc/wireguard/
execSync(`sudo wg-quick up "${configPath}"`, {
encoding: 'utf8',
timeout: 15000
});
activeInterfaces.set(interfaceName, {
configPath,
startedAt: Date.now(),
sites: new Set()
});
if (forceDebug) {
console.log(formatLogMessage('debug', `[vpn] Interface ${interfaceName} is up`));
}
const externalIP = getExternalIP(interfaceName);
return { success: true, interface: interfaceName, externalIP };
} catch (error) {
return {
success: false,
interface: interfaceName,
error: error.message.trim()
};
}
}
/**
* Bring down a WireGuard interface
* @param {string} interfaceName - Interface name
* @param {boolean} forceDebug - Debug logging
* @returns {Object} { success, error }
*/
function interfaceDown(interfaceName, forceDebug = false) {
const info = activeInterfaces.get(interfaceName);
if (!info) {
return { success: true, alreadyDown: true };
}
try {
execSync(`sudo wg-quick down "${info.configPath}"`, {
encoding: 'utf8',
timeout: 10000
});
activeInterfaces.delete(interfaceName);
// Clean up temp config if it was inline
const tempPath = path.join(TEMP_CONFIG_DIR, `${interfaceName}.conf`);
if (fs.existsSync(tempPath)) {
try { fs.unlinkSync(tempPath); } catch {}
}
if (forceDebug) {
console.log(formatLogMessage('debug', `[vpn] Interface ${interfaceName} is down`));
}
return { success: true };
} catch (error) {
// Force remove from tracking even if wg-quick fails
activeInterfaces.delete(interfaceName);
return { success: false, error: error.message.trim() };
}
}
/**
* Check if a WireGuard interface is connected and passing traffic
* @param {string} interfaceName - Interface name
* @param {string} testHost - Host to ping (default: 1.1.1.1)
* @param {boolean} forceDebug - Debug logging
* @returns {Object} { connected, latencyMs, error }
*/
function checkConnection(interfaceName, testHost = '1.1.1.1', forceDebug = false) {
try {
// Check interface exists
execSync(`ip link show ${interfaceName}`, { encoding: 'utf8', timeout: 3000 });
// Ping through the specific interface
const result = execSync(
`ping -c 1 -W 5 -I ${interfaceName} ${testHost} 2>&1`,
{ encoding: 'utf8', timeout: 8000 }
);
const latencyMatch = result.match(/time=([0-9.]+)\s*ms/);
const latencyMs = latencyMatch ? parseFloat(latencyMatch[1]) : null;
if (forceDebug) {
console.log(formatLogMessage('debug',
`[vpn] ${interfaceName} connected (${latencyMs ? latencyMs + 'ms' : 'ok'})`
));
}
return { connected: true, latencyMs };
} catch (error) {
if (forceDebug) {
console.log(formatLogMessage('debug',
`[vpn] ${interfaceName} health check failed: ${error.message.split('\n')[0]}`
));
}
return { connected: false, error: error.message.split('\n')[0] };
}
}
/**
* Get WireGuard status for an interface
* @param {string} interfaceName - Interface name
* @returns {Object} Parsed wg show output
*/
function getInterfaceStatus(interfaceName) {
try {
const output = execSync(`wg show ${interfaceName}`, {
encoding: 'utf8',
timeout: 5000
});
const status = { interface: interfaceName, raw: output };
// Parse key fields
const endpointMatch = output.match(/endpoint:\s*(.+)/);
const transferMatch = output.match(/transfer:\s*(.+)/);
const handshakeMatch = output.match(/latest handshake:\s*(.+)/);
const allowedMatch = output.match(/allowed ips:\s*(.+)/);
if (endpointMatch) status.endpoint = endpointMatch[1].trim();
if (transferMatch) status.transfer = transferMatch[1].trim();
if (handshakeMatch) status.latestHandshake = handshakeMatch[1].trim();
if (allowedMatch) status.allowedIps = allowedMatch[1].trim();
return status;
} catch (error) {
return { interface: interfaceName, error: error.message.split('\n')[0] };
}
}
/**
* Parse and validate a VPN site config
* @param {Object|string} vpnConfig - VPN config from site JSON
* @returns {Object} Normalized config { config, config_inline, interface, health_check, ... }
*/
function normalizeVpnConfig(vpnConfig) {
// String shorthand: just a path to config
if (typeof vpnConfig === 'string') {
return { config: vpnConfig, interface: null, health_check: true };
}
if (typeof vpnConfig !== 'object' || vpnConfig === null) {
return null;
}
return {
config: vpnConfig.config || null,
config_inline: vpnConfig.config_inline || null,
interface: vpnConfig.interface || null,
health_check: vpnConfig.health_check !== false,
test_host: vpnConfig.test_host || '1.1.1.1',
retry: vpnConfig.retry !== false,
max_retries: vpnConfig.max_retries || 2
};
}
/**
* Validate a VPN configuration
* @param {Object} vpnConfig - Normalized VPN config
* @returns {Object} { isValid, errors, warnings }
*/
function validateVpnConfig(vpnConfig) {
const result = { isValid: true, errors: [], warnings: [] };
if (!vpnConfig) {
result.isValid = false;
result.errors.push('VPN configuration is null or invalid');
return result;
}
if (!vpnConfig.config && !vpnConfig.config_inline) {
result.isValid = false;
result.errors.push('VPN requires either "config" (path) or "config_inline" (content)');
return result;
}
if (vpnConfig.config && vpnConfig.config_inline) {
result.warnings.push('Both "config" and "config_inline" provided; "config" takes precedence');
}
// Validate config file exists
if (vpnConfig.config) {
const configPath = vpnConfig.config;
// Accept both with and without .conf extension
const pathsToCheck = [configPath, `${configPath}.conf`, `/etc/wireguard/${configPath}.conf`];
const found = pathsToCheck.some(p => fs.existsSync(p));
if (!found) {
result.isValid = false;
result.errors.push(`Config file not found: ${configPath} (also checked /etc/wireguard/)`);
}
}
// Validate inline config has required sections
if (vpnConfig.config_inline && !vpnConfig.config) {
const content = vpnConfig.config_inline;
if (!content.includes('[Interface]')) {
result.isValid = false;
result.errors.push('Inline config missing [Interface] section');
}
if (!content.includes('[Peer]')) {
result.isValid = false;
result.errors.push('Inline config missing [Peer] section');
}
if (!content.includes('PrivateKey')) {
result.isValid = false;
result.errors.push('Inline config missing PrivateKey');
}
}
if (!hasRootPrivileges()) {
result.warnings.push('WireGuard requires root privileges - run with sudo');
}
return result;
}
/**
* Bring up VPN for a site, with health check and retry
* @param {Object} siteConfig - Site configuration from JSON
* @param {boolean} forceDebug - Debug logging
* @returns {Promise<Object>} { success, interface, error }
*/
async function connectForSite(siteConfig, forceDebug = false) {
const vpnConfig = normalizeVpnConfig(siteConfig.vpn);
if (!vpnConfig) {
return { success: false, error: 'Invalid VPN configuration' };
}
const validation = validateVpnConfig(vpnConfig);
if (!validation.isValid) {
return { success: false, error: validation.errors.join('; ') };
}
const interfaceName = resolveInterfaceName(vpnConfig);
// Resolve config path
let configPath;
if (vpnConfig.config) {
configPath = vpnConfig.config;
// Resolve wg-quick style: if no path separators, look in /etc/wireguard/
if (!configPath.includes('/')) {
const etcPath = `/etc/wireguard/${configPath}.conf`;
if (fs.existsSync(etcPath)) {
configPath = etcPath;
}
}
} else {
configPath = writeInlineConfig(interfaceName, vpnConfig.config_inline);
}
const maxAttempts = vpnConfig.retry ? vpnConfig.max_retries + 1 : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (forceDebug && attempt > 1) {
console.log(formatLogMessage('debug',
`[vpn] Retry ${attempt - 1}/${vpnConfig.max_retries} for ${interfaceName}`
));
}
// Ensure interface is down before retry
if (attempt > 1) {
interfaceDown(interfaceName, forceDebug);
await new Promise(resolve => setTimeout(resolve, 2000));
}
const upResult = interfaceUp(configPath, interfaceName, forceDebug);
if (!upResult.success) {
if (attempt === maxAttempts) {
return upResult;
}
continue;
}
// Track which site is using this interface
const info = activeInterfaces.get(interfaceName);
if (info && siteConfig.url) {
info.sites.add(siteConfig.url);
}
// Health check
if (vpnConfig.health_check) {
// Brief settle time for interface
await new Promise(resolve => setTimeout(resolve, 1500));
const health = checkConnection(interfaceName, vpnConfig.test_host, forceDebug);
if (!health.connected) {
if (attempt === maxAttempts) {
interfaceDown(interfaceName, forceDebug);
return {
success: false,
interface: interfaceName,
error: `Health check failed: ${health.error}`
};
}
continue;
}
}
return { success: true, interface: interfaceName };
}
return { success: false, interface: interfaceName, error: 'All attempts failed' };
}
/**
* Disconnect VPN for a site
* If multiple sites share the interface, only removes the site reference
* @param {Object} siteConfig - Site configuration from JSON
* @param {boolean} forceDebug - Debug logging
* @returns {Object} { success, tornDown, error }
*/
function disconnectForSite(siteConfig, forceDebug = false) {
const vpnConfig = normalizeVpnConfig(siteConfig.vpn);
if (!vpnConfig) return { success: true, tornDown: false };
const interfaceName = resolveInterfaceName(vpnConfig);
const info = activeInterfaces.get(interfaceName);
if (!info) {
return { success: true, tornDown: false };
}
// Remove this site from the interface's site set
if (siteConfig.url) {
info.sites.delete(siteConfig.url);
}
// Only tear down if no other sites are using it
if (info.sites.size === 0) {
const result = interfaceDown(interfaceName, forceDebug);
return { success: result.success, tornDown: true, error: result.error };
}
if (forceDebug) {
console.log(formatLogMessage('debug',
`[vpn] ${interfaceName} still used by ${info.sites.size} site(s), keeping up`
));
}
return { success: true, tornDown: false };
}
/**
* Tear down all active WireGuard interfaces
* Call on process exit or cleanup
* @param {boolean} forceDebug - Debug logging
* @returns {Object} { tornDown, errors }
*/
function disconnectAll(forceDebug = false) {
const results = { tornDown: 0, errors: [] };
for (const [interfaceName] of activeInterfaces) {
const result = interfaceDown(interfaceName, forceDebug);
if (result.success) {
results.tornDown++;
} else {
results.errors.push({ interface: interfaceName, error: result.error });
}
}
// Clean up temp directory
if (fs.existsSync(TEMP_CONFIG_DIR)) {
try { fs.rmSync(TEMP_CONFIG_DIR, { recursive: true, force: true }); } catch {}
}
if (forceDebug && results.tornDown > 0) {
console.log(formatLogMessage('debug',
`[vpn] Disconnected ${results.tornDown} interface(s)`
));
}
return results;
}
/**
* Get summary of all active VPN interfaces
* @returns {Array} Array of interface status objects
*/
function getActiveInterfaces() {
const interfaces = [];
for (const [name, info] of activeInterfaces) {
const status = getInterfaceStatus(name);
interfaces.push({
name,
configPath: info.configPath,
uptime: Math.round((Date.now() - info.startedAt) / 1000),
sites: Array.from(info.sites),
...status
});
}
return interfaces;
}
module.exports = {
validateWireGuardAvailability,
validateVpnConfig,
normalizeVpnConfig,
connectForSite,
disconnectForSite,
disconnectAll,
checkConnection,
getInterfaceStatus,
getActiveInterfaces,
// Low-level (for testing or advanced use)
interfaceUp,
interfaceDown,
resolveInterfaceName,
hasRootPrivileges
};