const express = require('express'); const router = express.Router(); const { authenticate } = require('../middleware/auth'); const { db } = require('../database/db'); const { spawn } = require('child_process'); const fs = require('fs').promises; const path = require('path'); const { modifyLimiter, readLimiter } = require('../middleware/rateLimiter'); const encryption = require('../utils/encryption'); // Use centralized encryption module for CWE-311 compliance const encrypt = (data) => encryption.encryptVPN(data); const decrypt = (data) => encryption.decryptVPN(data); // Parse WireGuard config function parseWireGuardConfig(content) { // Security: Limit content size if (content.length > 100000) { throw new Error('Configuration file too large'); } // Security: Check for script injection attempts if (content.includes(' 0) { const value = valueParts.join('=').trim(); // Security: Sanitize values, no shell commands if (value.includes('$(') || value.includes('`') || value.includes(';')) { throw new Error('Invalid configuration value detected'); } parsed[section][key.trim()] = value; } } } // Validate required fields console.log('[VPN-CONFIG] Parsed interface:', Object.keys(parsed.interface)); console.log('[VPN-CONFIG] Parsed peer:', Object.keys(parsed.peer)); if (!parsed.interface.PrivateKey || !parsed.peer.PublicKey || !parsed.peer.Endpoint) { console.log('[VPN-CONFIG] Missing fields - PrivateKey:', !!parsed.interface.PrivateKey, 'PublicKey:', !!parsed.peer.PublicKey, 'Endpoint:', !!parsed.peer.Endpoint); throw new Error('Missing required WireGuard configuration fields'); } // Extract metadata const endpoint = parsed.peer.Endpoint || ''; const country = extractCountryFromEndpoint(endpoint); const serverName = endpoint.split(':')[0]; return { type: 'wireguard', country, serverName, endpoint, data: parsed }; } // Parse OpenVPN config function parseOpenVPNConfig(content) { // Security: Limit content size if (content.length > 100000) { throw new Error('Configuration file too large'); } // Security: Check for script injection attempts if (content.includes(' { try { const configs = await new Promise((resolve, reject) => { db.all( `SELECT id, name, config_type, country, server_name, endpoint, is_active, created_at FROM vpn_configs WHERE user_id = ? ORDER BY is_active DESC, created_at DESC`, [req.user.userId], (err, rows) => { if (err) reject(err); else resolve(rows || []); } ); }); // Verify actual VPN connection status for active configs for (const config of configs) { if (config.is_active) { const actuallyConnected = await checkVPNConnection(config.config_type, req.user.userId); if (!actuallyConnected) { // Update database to reflect actual state await new Promise((resolve, reject) => { db.run( 'UPDATE vpn_configs SET is_active = 0 WHERE id = ?', [config.id], (err) => (err ? reject(err) : resolve()) ); }); config.is_active = 0; console.log(`[VPN-CONFIG] Reset stale active state for config ${config.id}`); } } } res.json({ configs }); } catch (err) { console.error('Error fetching VPN configs:', err); res.status(500).json({ error: 'Failed to fetch VPN configurations' }); } }); // Get specific config router.get('/configs/:id', authenticate, readLimiter, (req, res) => { db.get( `SELECT id, name, config_type, config_data, country, server_name, endpoint, is_active, created_at FROM vpn_configs WHERE id = ? AND user_id = ?`, [req.params.id, req.user.userId], (err, config) => { if (err) { console.error('Error fetching VPN config:', err); return res.status(500).json({ error: 'Failed to fetch configuration' }); } if (!config) { return res.status(404).json({ error: 'Configuration not found' }); } // Decrypt config data try { config.config_data = JSON.stringify(decrypt(config.config_data)); } catch (error) { console.error('Error decrypting config:', error); return res.status(500).json({ error: 'Failed to decrypt configuration' }); } res.json({ config }); } ); }); // Upload VPN config file router.post('/configs/upload', authenticate, modifyLimiter, async (req, res) => { try { console.log('[VPN-CONFIG] Upload request received'); console.log('[VPN-CONFIG] Files:', req.files ? Object.keys(req.files) : 'none'); // CWE-532: Do not log request body - may contain sensitive VPN credentials if (!req.files || !req.files.config) { return res.status(400).json({ error: 'No file uploaded' }); } const uploadedFile = req.files.config; // Validate file size (1MB max) if (uploadedFile.size > 1024 * 1024) { return res.status(400).json({ error: 'File too large (max 1MB)' }); } // Validate file extension const ext = path.extname(uploadedFile.name).toLowerCase(); if (ext !== '.conf' && ext !== '.ovpn') { return res.status(400).json({ error: 'Only .conf and .ovpn files are allowed' }); } const { name } = req.body; if (!name || name.trim().length === 0) { return res.status(400).json({ error: 'Configuration name is required' }); } // Validate name if (!/^[a-zA-Z0-9\s\-_.()]+$/.test(name)) { return res.status(400).json({ error: 'Invalid configuration name. Use only letters, numbers, spaces, and common punctuation.' }); } if (name.length > 100) { return res.status(400).json({ error: 'Configuration name too long (max 100 characters)' }); } // Read file content from temp file const content = await fs.readFile(uploadedFile.tempFilePath, 'utf8'); console.log('[VPN-CONFIG] File extension:', ext); console.log('[VPN-CONFIG] Content length:', content.length); console.log('[VPN-CONFIG] First 200 chars:', content.substring(0, 200)); // Parse based on file type let parsed; if (ext === '.conf') { parsed = parseWireGuardConfig(content); } else if (ext === '.ovpn') { parsed = parseOpenVPNConfig(content); } else { return res.status(400).json({ error: 'Unsupported file format' }); } // Encrypt config data using centralized encryption (CWE-311) const encryptedData = encrypt(parsed.data); // Save to database db.run( `INSERT INTO vpn_configs (user_id, name, config_type, config_data, country, server_name, endpoint, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, 0)`, [req.user.userId, name.trim(), parsed.type, encryptedData, parsed.country, parsed.serverName, parsed.endpoint], function(err) { if (err) { console.error('Error saving VPN config:', err); return res.status(500).json({ error: 'Failed to save configuration' }); } res.json({ message: 'Configuration uploaded successfully', config: { id: this.lastID, name: name.trim(), type: parsed.type, country: parsed.country, serverName: parsed.serverName, endpoint: parsed.endpoint } }); } ); } catch (error) { console.error('[VPN-CONFIG] Error processing config upload:', error); console.error('[VPN-CONFIG] Error stack:', error.stack); res.status(500).json({ error: error.message || 'Failed to process configuration file' }); } }); // Delete config router.delete('/configs/:id', authenticate, modifyLimiter, (req, res) => { // First check if config is active db.get( 'SELECT is_active FROM vpn_configs WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], (err, config) => { if (err) { console.error('Error checking config:', err); return res.status(500).json({ error: 'Failed to delete configuration' }); } if (!config) { return res.status(404).json({ error: 'Configuration not found' }); } if (config.is_active) { return res.status(400).json({ error: 'Cannot delete active configuration. Disconnect first.' }); } // Delete the config db.run( 'DELETE FROM vpn_configs WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], function(err) { if (err) { console.error('Error deleting config:', err); return res.status(500).json({ error: 'Failed to delete configuration' }); } res.json({ message: 'Configuration deleted successfully' }); } ); } ); }); // Set active config router.post('/configs/:id/activate', authenticate, modifyLimiter, (req, res) => { db.serialize(() => { // Deactivate all configs for user db.run( 'UPDATE vpn_configs SET is_active = 0 WHERE user_id = ?', [req.user.userId], (err) => { if (err) { console.error('Error deactivating configs:', err); return res.status(500).json({ error: 'Failed to activate configuration' }); } // Activate the selected config db.run( 'UPDATE vpn_configs SET is_active = 1 WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], function(err) { if (err) { console.error('Error activating config:', err); return res.status(500).json({ error: 'Failed to activate configuration' }); } if (this.changes === 0) { return res.status(404).json({ error: 'Configuration not found' }); } res.json({ message: 'Configuration activated successfully' }); } ); } ); }); }); // Connect using config router.post('/configs/:id/connect', authenticate, modifyLimiter, async (req, res) => { try { // Get config const config = await new Promise((resolve, reject) => { db.get( 'SELECT config_type, config_data FROM vpn_configs WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], (err, row) => { if (err) reject(err); else resolve(row); } ); }); if (!config) { return res.status(404).json({ error: 'Configuration not found' }); } // Decrypt config const decryptedData = decrypt(config.config_data); console.log(`[VPN-CONFIG] Connecting config ${req.params.id} for user ${req.user.userId}`); // Connect based on type if (config.config_type === 'wireguard') { await connectWireGuard(decryptedData, req.user.userId); } else if (config.config_type === 'openvpn') { await connectOpenVPN(decryptedData, req.user.userId); } console.log(`[VPN-CONFIG] Successfully connected, updating database`); // Deactivate all other configs for this user first await new Promise((resolve, reject) => { db.run( 'UPDATE vpn_configs SET is_active = 0 WHERE user_id = ?', [req.user.userId], (err) => (err ? reject(err) : resolve()) ); }); // Mark this config as active await new Promise((resolve, reject) => { db.run( 'UPDATE vpn_configs SET is_active = 1 WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], (err) => (err ? reject(err) : resolve()) ); }); // CWE-532: Logged without exposing sensitive config ID console.log(`[VPN-CONFIG] Configuration marked as active for user ${req.user.userId}`); res.json({ message: 'Connected to VPN successfully', success: true }); } catch (error) { console.error('Error connecting to VPN:', error); res.status(500).json({ error: error.message || 'Failed to connect to VPN' }); } }); // Disconnect VPN router.post('/configs/:id/disconnect', authenticate, modifyLimiter, async (req, res) => { try { // Get config const config = await new Promise((resolve, reject) => { db.get( 'SELECT config_type FROM vpn_configs WHERE id = ? AND user_id = ? AND is_active = 1', [req.params.id, req.user.userId], (err, row) => { if (err) reject(err); else resolve(row); } ); }); if (!config) { // No active config found, but still try to clean up any interfaces console.log('[VPN] No active config found, attempting cleanup anyway'); try { await disconnectWireGuard(req.user.userId); } catch (e) { // Ignore errors during cleanup } // Mark as inactive regardless await new Promise((resolve, reject) => { db.run( 'UPDATE vpn_configs SET is_active = 0 WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], (err) => (err ? reject(err) : resolve()) ); }); return res.json({ message: 'VPN state cleaned up' }); } // Disconnect based on type if (config.config_type === 'wireguard') { await disconnectWireGuard(req.user.userId); } else if (config.config_type === 'openvpn') { await disconnectOpenVPN(req.user.userId); } // Mark as inactive await new Promise((resolve, reject) => { db.run( 'UPDATE vpn_configs SET is_active = 0 WHERE id = ? AND user_id = ?', [req.params.id, req.user.userId], (err) => (err ? reject(err) : resolve()) ); }); res.json({ message: 'Disconnected from VPN successfully' }); } catch (error) { console.error('Error disconnecting from VPN:', error); res.status(500).json({ error: error.message || 'Failed to disconnect from VPN' }); } }); // Helper: Connect WireGuard async function connectWireGuard(config, userId) { const interfaceName = `wg${userId}`; const confPath = `/etc/wireguard/${interfaceName}.conf`; // Build WireGuard config with split-tunnel for local network access // Note: DNS is not set in config to avoid conflicts with Docker's DNS // Extract VPN endpoint IP to exclude from tunnel (prevent routing loop) const vpnEndpointIP = config.peer.Endpoint ? config.peer.Endpoint.split(':')[0] : null; // Use DNS from config (VPN provider's DNS) with public DNS fallbacks const dnsServers = config.interface.DNS || '1.1.1.1'; const primaryDNS = dnsServers.split(',')[0].trim(); const wgConfig = `[Interface] PrivateKey = ${config.interface.PrivateKey} Address = ${config.interface.Address} Table = off FwMark = 0xca6c PostUp = ip route add default dev %i table 51820${vpnEndpointIP ? ` PostUp = ip route add ${vpnEndpointIP}/32 via 172.20.0.1 dev eth0` : ''} PostUp = ip rule add to 172.20.0.0/16 table main priority 50 PostUp = ip rule add to 192.168.0.0/16 table main priority 51 PostUp = ip rule add not fwmark 0xca6c table 51820 priority 100 PostUp = ip route replace default via 172.20.0.1 dev eth0 metric 200 PostUp = ip route add default dev %i metric 50 PostUp = cp /etc/resolv.conf /etc/resolv.conf.vpn-backup PostUp = echo 'nameserver ${primaryDNS}' > /etc/resolv.conf PostUp = echo 'nameserver 1.1.1.1' >> /etc/resolv.conf PostUp = echo 'nameserver 8.8.8.8' >> /etc/resolv.conf PreDown = ip route del default dev %i metric 50 PreDown = ip route replace default via 172.20.0.1 dev eth0 metric 0 PreDown = ip rule del to 172.20.0.0/16 table main priority 50 PreDown = ip rule del to 192.168.0.0/16 table main priority 51 PreDown = ip rule del to 10.0.0.0/8 table main priority 52 PreDown = ip rule del not fwmark 0xca6c table 51820 priority 100 PreDown = ip route del default dev %i table 51820${vpnEndpointIP ? ` PreDown = ip route del ${vpnEndpointIP}/32 via 172.20.0.1 dev eth0` : ''} PreDown = mv /etc/resolv.conf.vpn-backup /etc/resolv.conf 2>/dev/null || true [Peer] PublicKey = ${config.peer.PublicKey} AllowedIPs = ${config.peer.AllowedIPs || '0.0.0.0/0, ::/0'} Endpoint = ${config.peer.Endpoint} ${config.peer.PersistentKeepalive ? `PersistentKeepalive = ${config.peer.PersistentKeepalive}` : ''} `; console.log('[WireGuard] Creating config file at', confPath); console.log('[WireGuard] Current user:', process.getuid ? process.getuid() : 'unknown'); // Write config file (this will work as root or with proper permissions) try { await fs.writeFile(confPath, wgConfig, { mode: 0o600 }); console.log('[WireGuard] Config file created successfully'); } catch (err) { console.error('[WireGuard] Failed to create config file:', err.message); throw new Error(`Failed to create WireGuard config: ${err.message}`); } return new Promise((resolve, reject) => { // Use shell to ensure root context const wg = spawn('wg-quick', ['up', interfaceName], { uid: 0, // Run as root gid: 0 }); let output = ''; wg.stdout.on('data', (data) => { output += data; console.log('[WireGuard]', data.toString().trim()); }); wg.stderr.on('data', (data) => { output += data; console.log('[WireGuard]', data.toString().trim()); }); wg.on('close', (code) => { if (code === 0) { console.log('[WireGuard] Connected successfully to', config.peer.Endpoint); resolve(); } else { console.error('[WireGuard] Connection failed (code', code, '):', output); // Check for Docker networking limitation if (output.includes('Nexthop has invalid gateway') || output.includes('Error: Nexthop')) { reject(new Error('VPN connection requires host network mode. Docker containers have limited network access. Please use the desktop app for VPN connections.')); } else { reject(new Error('WireGuard connection failed')); } } }); wg.on('error', (err) => { console.error('[WireGuard] Spawn error:', err.message); reject(new Error(`Failed to start wg-quick: ${err.message}`)); }); }); } // Helper: Connect OpenVPN async function connectOpenVPN(config, userId) { const confPath = `/tmp/ovpn_${userId}.conf`; await fs.writeFile(confPath, config.config); return new Promise((resolve, reject) => { const ovpn = spawn('openvpn', [ '--config', confPath, '--daemon', '--log', `/tmp/ovpn_${userId}.log` ]); ovpn.on('close', (code) => { if (code === 0) { console.log('[OpenVPN] Connected successfully'); resolve(); } else { reject(new Error('OpenVPN connection failed. VPN requires host network mode. Please use the desktop app for VPN connections.')); } }); }); } // Helper: Disconnect WireGuard async function disconnectWireGuard(userId) { const interfaceName = `wg${userId}`; console.log('[WireGuard] Disconnecting interface:', interfaceName); return new Promise((resolve, reject) => { const wg = spawn('wg-quick', ['down', interfaceName], { uid: 0, gid: 0 }); let output = ''; wg.stdout.on('data', (data) => { output += data; console.log('[WireGuard]', data.toString().trim()); }); wg.stderr.on('data', (data) => { output += data; console.log('[WireGuard]', data.toString().trim()); }); wg.on('close', (code) => { if (code === 0) { console.log('[WireGuard] Disconnected successfully'); resolve(); } else if (output.includes('is not a WireGuard interface') || output.includes('does not exist')) { // Interface doesn't exist - already disconnected console.log('[WireGuard] Interface already disconnected or config file missing'); resolve(); } else { console.error('[WireGuard] Disconnect failed (code', code, '):', output); reject(new Error('WireGuard disconnect failed')); } }); wg.on('error', (err) => { console.error('[WireGuard] Spawn error:', err.message); reject(new Error(`Failed to stop wg-quick: ${err.message}`)); }); }); } // Helper: Disconnect OpenVPN async function disconnectOpenVPN(userId) { console.log('[OpenVPN] Disconnecting...'); return new Promise((resolve, reject) => { // Kill OpenVPN process const pkill = spawn('pkill', ['-f', `openvpn.*ovpn_${userId}`]); pkill.on('close', (code) => { // pkill returns 0 if processes were killed, 1 if none were found if (code === 0 || code === 1) { console.log('[OpenVPN] Disconnected successfully'); resolve(); } else { console.error('[OpenVPN] Disconnect failed with code:', code); reject(new Error('OpenVPN disconnect failed')); } }); pkill.on('error', (err) => { console.error('[OpenVPN] Spawn error:', err.message); reject(new Error(`Failed to stop OpenVPN: ${err.message}`)); }); }); } // Helper: Check if VPN is actually connected async function checkVPNConnection(configType, userId) { try { if (configType === 'wireguard') { const interfaceName = `wg${userId}`; return new Promise((resolve) => { const wg = spawn('wg', ['show', interfaceName]); let hasOutput = false; wg.stdout.on('data', () => { hasOutput = true; }); wg.on('close', (code) => { resolve(code === 0 && hasOutput); }); wg.on('error', () => resolve(false)); }); } else if (configType === 'openvpn') { return new Promise((resolve) => { const pgrep = spawn('pgrep', ['-f', `openvpn.*ovpn_${userId}`]); let hasOutput = false; pgrep.stdout.on('data', () => { hasOutput = true; }); pgrep.on('close', (code) => { resolve(code === 0 && hasOutput); }); pgrep.on('error', () => resolve(false)); }); } return false; } catch (error) { return false; } } module.exports = router;