567 lines
23 KiB
JavaScript
567 lines
23 KiB
JavaScript
const { exec } = require('child_process');
|
|
const os = require('os');
|
|
const util = require('util');
|
|
const state = require('../infrastructure/state');
|
|
const { logDebug, logError, logWarn, logInfo } = require('../infrastructure/logger');
|
|
|
|
const execAsync = util.promisify(exec);
|
|
|
|
// Helper function to calculate available IPs for a subnet
|
|
function getAvailableIPsForSubnet(subnet) {
|
|
const maxIPs = Math.pow(2, 32 - subnet.cidr) - 2; // Subtract network and broadcast
|
|
const startIndex = subnet.startIndex || 2;
|
|
const endIndex = Math.min(254, maxIPs); // Cap at 254 for /24 and smaller
|
|
return Math.max(0, endIndex - startIndex + 1);
|
|
}
|
|
|
|
// Helper function to check if an IP belongs to any configured subnet
|
|
function ipBelongsToSubnet(ip, subnet) {
|
|
const ipParts = ip.split('.').map(p => parseInt(p, 10));
|
|
const subnetParts = subnet.base.split('.').map(p => parseInt(p, 10));
|
|
|
|
// Calculate network mask
|
|
const maskBits = subnet.cidr;
|
|
const maskParts = [];
|
|
for (let i = 0; i < 4; i++) {
|
|
const bits = Math.min(8, Math.max(0, maskBits - i * 8));
|
|
maskParts.push(bits === 8 ? 255 : bits === 0 ? 0 : (255 << (8 - bits)) & 255);
|
|
}
|
|
|
|
// Check if IP is in subnet
|
|
for (let i = 0; i < 4; i++) {
|
|
if ((ipParts[i] & maskParts[i]) !== (subnetParts[i] & maskParts[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Helper function to generate IP from subnet and index
|
|
function generateIPFromSubnet(subnet, index) {
|
|
const baseParts = subnet.base.split('.').map(p => parseInt(p, 10));
|
|
const lastOctet = index;
|
|
return `${baseParts[0]}.${baseParts[1]}.${baseParts[2]}.${lastOctet}`;
|
|
}
|
|
|
|
// Helper function to check if base interface is up
|
|
async function ensureBaseInterfaceUp() {
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
return true;
|
|
}
|
|
if (!state.subnetName) {
|
|
return false;
|
|
}
|
|
try {
|
|
let command;
|
|
if (os.platform() === 'darwin') {
|
|
command = `ifconfig ${state.subnetName} | grep -q 'status: active' || ifconfig ${state.subnetName} | grep -q 'UP'`;
|
|
} else if (os.platform() === 'linux') {
|
|
command = `ip link show ${state.subnetName} | grep -q 'state UP'`;
|
|
} else if (os.platform() === 'win32') {
|
|
// Windows loopback interfaces are typically always up
|
|
return true;
|
|
} else {
|
|
logWarn('VirtualInterface', `Unsupported platform: ${os.platform()}, cannot verify interface state`);
|
|
return false;
|
|
}
|
|
await execAsync(command);
|
|
return true;
|
|
} catch (err) {
|
|
// Interface is not up, try to bring it up
|
|
logDebug('VirtualInterface', `Base interface ${state.subnetName} is not up, attempting to bring it up`);
|
|
try {
|
|
let upCommand;
|
|
if (os.platform() === 'darwin') {
|
|
// macOS loopback interfaces are typically always up, but we can verify
|
|
upCommand = `ifconfig ${state.subnetName} up`;
|
|
} else if (os.platform() === 'linux') {
|
|
upCommand = `sudo ip link set ${state.subnetName} up`;
|
|
} else {
|
|
return false;
|
|
}
|
|
await execAsync(upCommand);
|
|
logInfo('VirtualInterface', `Brought base interface ${state.subnetName} up`);
|
|
return true;
|
|
} catch (upErr) {
|
|
logWarn('VirtualInterface', `Could not bring base interface ${state.subnetName} up: ${upErr.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to verify interface state after IP assignment
|
|
async function verifyInterfaceState(ip) {
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
return true;
|
|
}
|
|
try {
|
|
let command;
|
|
if (os.platform() === 'darwin') {
|
|
command = `ifconfig ${state.subnetName} | grep 'inet ${ip}'`;
|
|
} else if (os.platform() === 'linux') {
|
|
command = `ip addr show ${state.subnetName} | grep 'inet ${ip}/'`;
|
|
} else if (os.platform() === 'win32') {
|
|
// Windows: Verify IP is configured
|
|
command = `netsh interface ip show addresses "${state.subnetName || 'Loopback Pseudo-Interface 1'}" | findstr "${ip}"`;
|
|
} else {
|
|
return false;
|
|
}
|
|
await execAsync(command);
|
|
return true;
|
|
} catch (err) {
|
|
logWarn('VirtualInterface', `Could not verify interface state for IP ${ip}: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Helper function to get all configured IPs on the interface
|
|
async function getConfiguredIPs() {
|
|
// If virtual interfaces are disabled, return empty array
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
return [];
|
|
}
|
|
try {
|
|
let command;
|
|
if (os.platform() === 'darwin') {
|
|
command = `ifconfig ${state.subnetName} | grep 'inet ' | awk '{print $2}'`;
|
|
} else if (os.platform() === 'linux') {
|
|
command = `ip addr show ${state.subnetName} | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1`;
|
|
} else if (os.platform() === 'win32') {
|
|
// Windows: Get IP addresses using netsh
|
|
command = `netsh interface ip show addresses "${state.subnetName || 'Loopback Pseudo-Interface 1'}" | findstr "IP Address"`;
|
|
} else {
|
|
logWarn('VirtualInterface', `Unsupported platform: ${os.platform()}, assuming no configured IPs`);
|
|
return [];
|
|
}
|
|
const { stdout } = await execAsync(command);
|
|
if (os.platform() === 'win32') {
|
|
// Parse Windows output: "IP Address: 192.168.3.2"
|
|
return stdout.split('\n')
|
|
.map(line => {
|
|
const match = line.match(/IP Address:\s*(\d+\.\d+\.\d+\.\d+)/);
|
|
return match ? match[1] : null;
|
|
})
|
|
.filter(ip => ip);
|
|
}
|
|
return stdout.split('\n').map(ip => ip.trim()).filter(ip => ip);
|
|
} catch (err) {
|
|
logError('VirtualInterface', `Error listing configured IPs: ${err.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Clean up invalid or stale IPs from state.domainToIPMap at startup
|
|
async function cleanInvalidIPs() {
|
|
// Skip cleanup if virtual interfaces are disabled
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
logDebug('VirtualInterface', 'Virtual interfaces disabled, skipping IP cleanup');
|
|
return;
|
|
}
|
|
|
|
// Get list of plugin/internal domains (these use 127.0.0.1)
|
|
let pluginDomains = [];
|
|
try {
|
|
const { getInternalDomains } = require('../plugins/plugin-handler');
|
|
pluginDomains = await getInternalDomains();
|
|
} catch (err) {
|
|
logDebug('VirtualInterface', `Could not get plugin domains during cleanup: ${err.message}`);
|
|
}
|
|
const pluginDomainsSet = new Set(pluginDomains);
|
|
|
|
const configuredIPs = await getConfiguredIPs();
|
|
const validSubnets = state.subnets || [];
|
|
|
|
// If no subnets configured, fall back to legacy SUBNET_BASE check
|
|
const legacySubnetBase = process.env.SUBNET_BASE || '192.168.3.';
|
|
|
|
for (const [domain, ip] of state.domainToIPMap) {
|
|
// Skip cleanup for plugin/internal domains (they use 127.0.0.1)
|
|
if (pluginDomainsSet.has(domain)) {
|
|
if (ip === '127.0.0.1') {
|
|
logDebug('VirtualInterface', `Preserving plugin domain ${domain} with IP 127.0.0.1`);
|
|
continue;
|
|
} else {
|
|
// Plugin domain had wrong IP, update it to 127.0.0.1
|
|
logWarn('VirtualInterface', `Plugin domain ${domain} had wrong IP ${ip}, updating to 127.0.0.1`);
|
|
if (configuredIPs.includes(ip)) {
|
|
await removeVirtualInterface(ip);
|
|
}
|
|
state.domainToIPMap.set(domain, '127.0.0.1');
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// For non-plugin domains, validate IP belongs to configured subnets
|
|
const ipParts = ip.split('.');
|
|
let isValid = false;
|
|
|
|
// Basic IP format validation
|
|
if (ipParts.length !== 4 || ipParts.some(part => isNaN(part) || part < 0 || part > 255)) {
|
|
isValid = false;
|
|
} else if (validSubnets.length > 0) {
|
|
// Check if IP belongs to any configured subnet
|
|
isValid = validSubnets.some(subnet => ipBelongsToSubnet(ip, subnet));
|
|
} else {
|
|
// Legacy: check if IP starts with SUBNET_BASE
|
|
isValid = ip.startsWith(legacySubnetBase);
|
|
}
|
|
|
|
if (!isValid) {
|
|
logWarn('VirtualInterface', `Removing invalid or stale IP ${ip} for domain ${domain} from state`);
|
|
if (configuredIPs.includes(ip)) {
|
|
await removeVirtualInterface(ip);
|
|
}
|
|
state.domainToIPMap.delete(domain);
|
|
} else if (configuredIPs.includes(ip)) {
|
|
logDebug('VirtualInterface', `IP ${ip} for domain ${domain} is valid and configured`);
|
|
} else {
|
|
logWarn('VirtualInterface', `IP ${ip} for domain ${domain} not configured, removing from state`);
|
|
state.domainToIPMap.delete(domain);
|
|
}
|
|
}
|
|
}
|
|
cleanInvalidIPs();
|
|
|
|
// Helper function to remove virtual interface
|
|
async function removeVirtualInterface(ip) {
|
|
return new Promise((resolve) => {
|
|
// Check if virtual interfaces are disabled
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
logDebug('VirtualInterface', 'Virtual interfaces disabled, skipping interface removal');
|
|
resolve();
|
|
return;
|
|
}
|
|
if (!state.subnetName) {
|
|
logWarn('VirtualInterface', 'SUBNET_NAME not defined, skipping interface removal');
|
|
resolve();
|
|
return;
|
|
}
|
|
// Validate IP format
|
|
const ipParts = ip.split('.');
|
|
if (ipParts.length !== 4 || ipParts.some(part => isNaN(part) || part < 0 || part > 255)) {
|
|
logWarn('VirtualInterface', `Skipping removal of invalid IP ${ip}`);
|
|
resolve();
|
|
return;
|
|
}
|
|
// Check if IP is configured
|
|
getConfiguredIPs().then(configuredIPs => {
|
|
if (!configuredIPs.includes(ip)) {
|
|
logDebug('VirtualInterface', `IP ${ip} not configured on system, skipping removal`);
|
|
resolve();
|
|
return;
|
|
}
|
|
const cidrIp = `${ip}/24`;
|
|
let command;
|
|
if (os.platform() === 'darwin') {
|
|
command = `sudo ifconfig ${state.subnetName} -alias ${ip}`;
|
|
} else if (os.platform() === 'linux') {
|
|
command = `sudo ip addr del ${cidrIp} dev ${state.subnetName}`;
|
|
} else if (os.platform() === 'win32') {
|
|
// Windows: Remove IP address using netsh
|
|
const interfaceName = state.subnetName || 'Loopback Pseudo-Interface 1';
|
|
command = `netsh interface ip delete address "${interfaceName}" ${ip}`;
|
|
} else {
|
|
logWarn('VirtualInterface', `Unsupported platform: ${os.platform()}, skipping interface removal`);
|
|
resolve();
|
|
return;
|
|
}
|
|
exec(command, (err, stdout, stderr) => {
|
|
if (err) {
|
|
logError('VirtualInterface', `Error removing virtual interface ${ip}: ${stderr || err.message}`);
|
|
} else {
|
|
logInfo('VirtualInterface', `Removed virtual interface IP: ${ip}`);
|
|
}
|
|
resolve();
|
|
});
|
|
}).catch(err => {
|
|
logError('VirtualInterface', `Error checking if IP ${ip} exists: ${err.message}`);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper function to create virtual interface
|
|
async function createVirtualInterface(ip) {
|
|
// Check if virtual interfaces are disabled
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
logDebug('VirtualInterface', 'Virtual interfaces disabled, skipping interface creation');
|
|
return ip; // Return the IP even though we're not creating the interface
|
|
}
|
|
// Check if IP is already configured
|
|
const configuredIPs = await getConfiguredIPs();
|
|
if (configuredIPs.includes(ip)) {
|
|
logWarn('VirtualInterface', `IP ${ip} already configured, using it`);
|
|
// Verify interface is up for ICMP
|
|
const isUp = await ensureBaseInterfaceUp();
|
|
if (isUp) {
|
|
logDebug('VirtualInterface', `Interface ${state.subnetName} is up, ICMP should be functional for ${ip}`);
|
|
}
|
|
return ip;
|
|
}
|
|
await removeVirtualInterface(ip); // Ensure IP is not already assigned
|
|
|
|
// Ensure base interface is up before adding IPs
|
|
const baseInterfaceUp = await ensureBaseInterfaceUp();
|
|
if (!baseInterfaceUp) {
|
|
logWarn('VirtualInterface', `Base interface ${state.subnetName} is not up, ICMP may not work for ${ip}`);
|
|
}
|
|
|
|
if (!state.subnetName) {
|
|
logWarn('VirtualInterface', 'Virtual interfaces not supported, skipping');
|
|
return ip;
|
|
}
|
|
|
|
const cidr = '/24';
|
|
const mask = '255.255.255.0';
|
|
let command;
|
|
if (os.platform() === 'darwin') {
|
|
command = `sudo ifconfig ${state.subnetName} alias ${ip} ${mask}`;
|
|
} else if (os.platform() === 'linux') {
|
|
command = `sudo ip addr add ${ip}${cidr} dev ${state.subnetName}`;
|
|
} else if (os.platform() === 'win32') {
|
|
// Windows: Add IP address using netsh
|
|
const interfaceName = state.subnetName || 'Loopback Pseudo-Interface 1';
|
|
command = `netsh interface ip add address "${interfaceName}" ${ip} ${mask}`;
|
|
} else {
|
|
logWarn('VirtualInterface', `Unsupported platform: ${os.platform()}, skipping interface creation`);
|
|
return ip;
|
|
}
|
|
|
|
try {
|
|
const { stdout, stderr } = await execAsync(command);
|
|
logInfo('VirtualInterface', `Created virtual interface with IP ${ip}`);
|
|
|
|
// Platform-specific post-creation steps for ICMP support
|
|
try {
|
|
if (os.platform() === 'linux') {
|
|
// On Linux, ensure the interface is up after adding IP
|
|
// This is critical for ICMP to work
|
|
const upCommand = `sudo ip link set ${state.subnetName} up`;
|
|
try {
|
|
await execAsync(upCommand);
|
|
logDebug('VirtualInterface', `Ensured interface ${state.subnetName} is up for ICMP support`);
|
|
} catch (upErr) {
|
|
logWarn('VirtualInterface', `Could not bring interface up: ${upErr.message}, ICMP may not work`);
|
|
}
|
|
} else if (os.platform() === 'darwin') {
|
|
// On macOS, verify the interface state after alias creation
|
|
const verified = await verifyInterfaceState(ip);
|
|
if (verified) {
|
|
logDebug('VirtualInterface', `Verified interface state for ${ip}, ICMP should be functional`);
|
|
} else {
|
|
logWarn('VirtualInterface', `Could not verify interface state for ${ip}, ICMP may not work`);
|
|
}
|
|
} else if (os.platform() === 'win32') {
|
|
// Windows loopback interfaces typically respond to ICMP automatically
|
|
const verified = await verifyInterfaceState(ip);
|
|
if (verified) {
|
|
logDebug('VirtualInterface', `Verified IP ${ip} is configured, ICMP should be functional`);
|
|
}
|
|
}
|
|
|
|
// Log that ICMP should now be functional
|
|
// Note: Firewall rules may still block ICMP, but the interface is configured correctly
|
|
logInfo('VirtualInterface', `Interface ${ip} is configured and ready for ICMP (ping) requests. Note: Ensure firewall allows ICMP if ping fails.`);
|
|
} catch (verifyErr) {
|
|
logWarn('VirtualInterface', `Error during post-creation verification for ${ip}: ${verifyErr.message}`);
|
|
}
|
|
|
|
return ip;
|
|
} catch (err) {
|
|
const errorMessage = err.stderr || err.message || '';
|
|
if (errorMessage.includes('Address already assigned') || errorMessage.includes('File exists')) {
|
|
logWarn('VirtualInterface', `IP ${ip} already assigned, using it`);
|
|
return ip;
|
|
} else {
|
|
logError('VirtualInterface', `Error creating virtual interface ${ip}: ${errorMessage}`);
|
|
throw new Error(`Error creating virtual interface ${ip}: ${errorMessage}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to create interface for domain with round-robin across multiple subnets
|
|
async function createInterfaceForDomain(domain) {
|
|
// Check if this is a plugin/internal domain (these run on 127.0.0.1 and don't need virtual interfaces)
|
|
try {
|
|
const { getInternalDomains } = require('../plugins/plugin-handler');
|
|
const internalDomains = await getInternalDomains();
|
|
if (internalDomains.includes(domain)) {
|
|
const pluginIP = '127.0.0.1';
|
|
const existingIP = state.domainToIPMap.get(domain);
|
|
|
|
// If domain already has a virtual IP, remove it and update to 127.0.0.1
|
|
if (existingIP && existingIP !== pluginIP) {
|
|
logWarn('VirtualInterface', `Plugin domain ${domain} had virtual IP ${existingIP}, removing virtual interface and updating to 127.0.0.1`);
|
|
try {
|
|
const configuredIPs = await getConfiguredIPs();
|
|
if (configuredIPs.includes(existingIP)) {
|
|
await removeVirtualInterface(existingIP);
|
|
}
|
|
} catch (removeErr) {
|
|
logWarn('VirtualInterface', `Error removing virtual interface for plugin domain: ${removeErr.message}`);
|
|
}
|
|
}
|
|
|
|
// Always set plugin domains to 127.0.0.1
|
|
state.domainToIPMap.set(domain, pluginIP);
|
|
logDebug('VirtualInterface', `Domain ${domain} is a plugin/internal domain, using 127.0.0.1 (no virtual interface created)`);
|
|
return pluginIP;
|
|
}
|
|
} catch (err) {
|
|
// If plugin handler is not available, continue with normal flow
|
|
logDebug('VirtualInterface', `Could not check if ${domain} is a plugin domain: ${err.message}`);
|
|
}
|
|
|
|
// Check if virtual interfaces are disabled
|
|
if (process.env.DISABLE_VIRTUAL_INTERFACES === 'true') {
|
|
logDebug('VirtualInterface', `Virtual interfaces disabled, skipping interface creation for domain: ${domain}`);
|
|
// Still return an IP for the domain mapping, but don't create the interface
|
|
// Generate a placeholder IP that won't be used for actual interface creation
|
|
if (!state.domainToIPMap.has(domain)) {
|
|
// Generate a placeholder IP based on domain hash to ensure consistency
|
|
const crypto = require('crypto');
|
|
const hash = crypto.createHash('md5').update(domain).digest('hex');
|
|
const ipIndex = parseInt(hash.substring(0, 2), 16) % 253 + 2; // Ensure it's between 2-254
|
|
const subnetBase = process.env.SUBNET_BASE || '192.168.3.';
|
|
const placeholderIP = `${subnetBase}${ipIndex}`;
|
|
state.domainToIPMap.set(domain, placeholderIP);
|
|
logDebug('VirtualInterface', `Assigned placeholder IP: ${placeholderIP} for domain: ${domain} (interface not created)`);
|
|
}
|
|
return state.domainToIPMap.get(domain);
|
|
}
|
|
if (state.domainToIPMap.has(domain)) {
|
|
const existingIP = state.domainToIPMap.get(domain);
|
|
// Validate existing IP
|
|
const ipParts = existingIP.split('.');
|
|
if (ipParts.length === 4 && ipParts.every(part => !isNaN(part) && part >= 0 && part <= 255)) {
|
|
// Check if IP is still configured
|
|
const configuredIPs = await getConfiguredIPs();
|
|
if (configuredIPs.includes(existingIP)) {
|
|
logDebug('VirtualInterface', `Interface already assigned for domain: ${domain}, IP: ${existingIP}`);
|
|
return existingIP;
|
|
} else {
|
|
logWarn('VirtualInterface', `IP ${existingIP} for domain ${domain} no longer configured, reassigning`);
|
|
state.domainToIPMap.delete(domain);
|
|
}
|
|
} else {
|
|
logWarn('VirtualInterface', `Removing invalid existing IP ${existingIP} for domain ${domain}`);
|
|
await removeVirtualInterface(existingIP);
|
|
state.domainToIPMap.delete(domain);
|
|
}
|
|
}
|
|
|
|
const subnets = state.subnets || [];
|
|
let attempts = 0;
|
|
const maxAttempts = 50; // Increased for multiple subnets
|
|
const maxSubnetAttempts = 10; // Max attempts per subnet before moving to next
|
|
|
|
// If no subnets configured, fall back to legacy behavior
|
|
if (subnets.length === 0) {
|
|
const subnetBase = process.env.SUBNET_BASE || '192.168.3.';
|
|
while (attempts < maxAttempts) {
|
|
const subnetID = state.currentIP;
|
|
const ip = `${subnetBase}${subnetID}`;
|
|
try {
|
|
const ipParts = ip.split('.');
|
|
if (ipParts.length !== 4 || ipParts.some(part => isNaN(part) || part < 0 || part > 255)) {
|
|
throw new Error(`Invalid IP format: ${ip}`);
|
|
}
|
|
for (const [existingDomain, existingIP] of state.domainToIPMap) {
|
|
if (existingIP === ip && existingDomain !== domain) {
|
|
throw new Error(`IP ${ip} already assigned to ${existingDomain}`);
|
|
}
|
|
}
|
|
const assignedIP = await createVirtualInterface(ip);
|
|
state.domainToIPMap.set(domain, assignedIP);
|
|
logInfo('VirtualInterface', `Assigned virtual interface IP: ${assignedIP} for domain: ${domain}`);
|
|
state.currentIP++;
|
|
return assignedIP;
|
|
} catch (error) {
|
|
logError('VirtualInterface', `Failed to assign IP ${ip} for ${domain}: ${error.message}`);
|
|
state.currentIP++;
|
|
attempts++;
|
|
}
|
|
}
|
|
logError('VirtualInterface', `Exceeded max attempts to assign IP for domain: ${domain}`);
|
|
return null;
|
|
}
|
|
|
|
// Round-robin across multiple subnets
|
|
let subnetAttempts = 0;
|
|
let startSubnetIndex = state.currentSubnetIndex;
|
|
|
|
while (attempts < maxAttempts && subnetAttempts < maxSubnetAttempts * subnets.length) {
|
|
const subnetIndex = state.currentSubnetIndex;
|
|
const subnet = subnets[subnetIndex];
|
|
|
|
if (!subnet) {
|
|
logError('VirtualInterface', `Invalid subnet index: ${subnetIndex}`);
|
|
state.currentSubnetIndex = (state.currentSubnetIndex + 1) % subnets.length;
|
|
subnetAttempts++;
|
|
continue;
|
|
}
|
|
|
|
// Get current IP counter for this subnet
|
|
const currentIPIndex = state.subnetIPCounters.get(subnetIndex) || subnet.startIndex;
|
|
const maxIPs = Math.pow(2, 32 - subnet.cidr) - 2;
|
|
const maxIndex = Math.min(254, maxIPs); // Cap at 254
|
|
|
|
// Check if subnet is exhausted
|
|
if (currentIPIndex > maxIndex) {
|
|
logWarn('VirtualInterface', `Subnet ${subnet.name} (${subnet.base}/${subnet.cidr}) exhausted, moving to next`);
|
|
state.currentSubnetIndex = (state.currentSubnetIndex + 1) % subnets.length;
|
|
subnetAttempts++;
|
|
// If we've tried all subnets, break
|
|
if (state.currentSubnetIndex === startSubnetIndex && subnetAttempts > 0) {
|
|
logError('VirtualInterface', `All subnets exhausted, cannot assign IP for domain: ${domain}`);
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const ip = generateIPFromSubnet(subnet, currentIPIndex);
|
|
|
|
try {
|
|
// Validate IP format
|
|
const ipParts = ip.split('.');
|
|
if (ipParts.length !== 4 || ipParts.some(part => isNaN(part) || part < 0 || part > 255)) {
|
|
throw new Error(`Invalid IP format: ${ip}`);
|
|
}
|
|
|
|
// Check if IP is already assigned to another domain
|
|
for (const [existingDomain, existingIP] of state.domainToIPMap) {
|
|
if (existingIP === ip && existingDomain !== domain) {
|
|
throw new Error(`IP ${ip} already assigned to ${existingDomain}`);
|
|
}
|
|
}
|
|
|
|
const assignedIP = await createVirtualInterface(ip);
|
|
state.domainToIPMap.set(domain, assignedIP);
|
|
logInfo('VirtualInterface', `Assigned virtual interface IP: ${assignedIP} for domain: ${domain} from subnet ${subnet.name}`);
|
|
|
|
// Increment IP counter for this subnet and move to next subnet (round-robin)
|
|
state.subnetIPCounters.set(subnetIndex, currentIPIndex + 1);
|
|
state.currentSubnetIndex = (state.currentSubnetIndex + 1) % subnets.length;
|
|
|
|
return assignedIP;
|
|
} catch (error) {
|
|
logError('VirtualInterface', `Failed to assign IP ${ip} for ${domain}: ${error.message}`);
|
|
// Increment IP counter and try next IP in same subnet
|
|
state.subnetIPCounters.set(subnetIndex, currentIPIndex + 1);
|
|
attempts++;
|
|
subnetAttempts++;
|
|
}
|
|
}
|
|
|
|
logError('VirtualInterface', `Exceeded max attempts to assign IP for domain: ${domain}`);
|
|
return null;
|
|
}
|
|
|
|
module.exports = {
|
|
removeVirtualInterface,
|
|
createVirtualInterface,
|
|
createInterfaceForDomain,
|
|
getAvailableIPsForSubnet,
|
|
ipBelongsToSubnet,
|
|
generateIPFromSubnet
|
|
}; |