Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
738
backend/routes/vpn-configs.js
Normal file
738
backend/routes/vpn-configs.js
Normal file
|
|
@ -0,0 +1,738 @@
|
|||
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('<script') || content.includes('${') || content.includes('eval(')) {
|
||||
throw new Error('Invalid configuration content detected');
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
const parsed = {
|
||||
type: 'wireguard',
|
||||
interface: {},
|
||||
peer: {}
|
||||
};
|
||||
|
||||
let section = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed === '[Interface]') {
|
||||
section = 'interface';
|
||||
} else if (trimmed === '[Peer]') {
|
||||
section = 'peer';
|
||||
} else if (trimmed && !trimmed.startsWith('#') && section) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 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('<script') || content.includes('${') || content.includes('eval(')) {
|
||||
throw new Error('Invalid configuration content detected');
|
||||
}
|
||||
|
||||
// Security: Check for dangerous directives
|
||||
const dangerousDirectives = ['script-security 3', 'up /bin/sh', 'down /bin/sh', 'route-up /bin/sh'];
|
||||
for (const directive of dangerousDirectives) {
|
||||
if (content.includes(directive)) {
|
||||
throw new Error('Configuration contains potentially dangerous directives');
|
||||
}
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
const parsed = {
|
||||
type: 'openvpn',
|
||||
remote: null,
|
||||
port: null,
|
||||
proto: null
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith('remote ')) {
|
||||
const parts = trimmed.split(/\s+/);
|
||||
parsed.remote = parts[1];
|
||||
parsed.port = parts[2] || '1194';
|
||||
parsed.proto = parts[3] || 'udp';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!parsed.remote) {
|
||||
throw new Error('Missing required OpenVPN remote server');
|
||||
}
|
||||
|
||||
// Extract metadata
|
||||
const country = extractCountryFromEndpoint(parsed.remote);
|
||||
const serverName = parsed.remote || 'Unknown';
|
||||
|
||||
return {
|
||||
type: 'openvpn',
|
||||
country,
|
||||
serverName,
|
||||
endpoint: `${parsed.remote}:${parsed.port}`,
|
||||
proto: parsed.proto,
|
||||
data: { config: content }
|
||||
};
|
||||
}
|
||||
|
||||
// Extract country code from endpoint/hostname
|
||||
function extractCountryFromEndpoint(endpoint) {
|
||||
if (!endpoint) return null;
|
||||
|
||||
// Common patterns: us-01.server.com, node-us-01, 185.107.57.98 (Romania)
|
||||
const countryMatch = endpoint.match(/[-_]([a-z]{2})[-_\d]/i);
|
||||
if (countryMatch) {
|
||||
return countryMatch[1].toUpperCase();
|
||||
}
|
||||
|
||||
// Known IP ranges (basic lookup)
|
||||
const ipRanges = {
|
||||
'185.107.57': 'RO',
|
||||
'185.163.110': 'RO',
|
||||
'169.150': 'US',
|
||||
'103.107': 'JP'
|
||||
};
|
||||
|
||||
for (const [range, country] of Object.entries(ipRanges)) {
|
||||
if (endpoint.startsWith(range)) {
|
||||
return country;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all VPN configs for user
|
||||
router.get('/configs', authenticate, readLimiter, async (req, res) => {
|
||||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue