import * as plugins from './plugins.js'; import type { IFirewallConfig, IFirewallRule, INetworkManagerOptions, ITapDevice, TFirewallAction, TFirewallProtocol, TWireGuardConfig, } from './interfaces/index.js'; import { SmartVMError } from './interfaces/index.js'; type TShellExecResult = Awaited['execSpawn']>>; interface IParsedSubnet { networkAddress: number; broadcastAddress: number; cidr: number; subnetMask: string; } interface IParsedWireGuardConfig { setConfig: string; addresses: string[]; mtu?: number; } const FIREWALL_ACTION_TO_IPTABLES_TARGET: Record = { allow: 'ACCEPT', deny: 'DROP', }; /** * Manages host networking for Firecracker VMs. * Creates TAP devices, Linux bridges, and configures NAT for VM internet access. */ export class NetworkManager { private bridgeName: string; private subnetBase: string; private subnetCidr: number; private gatewayIp: string; private subnetMask: string; private nextIpAddress: number; private lastUsableIpAddress: number; private activeTaps: Map = new Map(); private bridgeCreated: boolean = false; private defaultRouteInterface: string | null = null; private firewall?: IFirewallConfig; private firewallChainName: string; private firewallConfigured: boolean = false; private wireguard?: TWireGuardConfig; private wireGuardInterface: string | null = null; private wireGuardManaged: boolean = false; private wireGuardRouteConfigured: boolean = false; private wireGuardRouteAdded: boolean = false; private wireGuardIpRuleAdded: boolean = false; private wireGuardRouteTable: number | null = null; private natInterface: string | null = null; private natConfigured: boolean = false; private natRuleAdded: boolean = false; private shell: InstanceType; constructor(options: INetworkManagerOptions = {}) { this.bridgeName = options.bridgeName || 'svbr0'; this.validateInterfaceName(this.bridgeName, 'bridgeName'); const subnet = options.subnet || '172.30.0.0/24'; const parsedSubnet = this.parseSubnet(subnet); this.subnetBase = this.intToIp(parsedSubnet.networkAddress); this.subnetCidr = parsedSubnet.cidr; this.subnetMask = parsedSubnet.subnetMask; this.gatewayIp = this.intToIp(parsedSubnet.networkAddress + 1); this.nextIpAddress = parsedSubnet.networkAddress + 2; this.lastUsableIpAddress = parsedSubnet.broadcastAddress - 1; this.firewall = options.firewall; this.wireguard = options.wireguard; this.firewallChainName = this.buildFirewallChainName(this.bridgeName, this.subnetBase, this.subnetCidr); this.validateFirewallConfig(this.firewall); this.validateWireGuardConfig(this.wireguard); this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); } /** * Parse an IPv4 CIDR subnet and ensure there is room for a gateway and guests. */ private parseSubnet(subnet: string): IParsedSubnet { const [ip, cidrText, extra] = subnet.split('/'); const cidr = Number(cidrText); if (!ip || !cidrText || extra !== undefined || !Number.isInteger(cidr) || cidr < 1 || cidr > 30) { throw new SmartVMError( `Invalid subnet '${subnet}': expected IPv4 CIDR with prefix length 1-30`, 'INVALID_SUBNET', ); } const ipAddress = this.ipToInt(ip); const mask = this.cidrToMask(cidr); const networkAddress = (ipAddress & mask) >>> 0; const hostCount = 2 ** (32 - cidr); const broadcastAddress = networkAddress + hostCount - 1; if (hostCount < 4) { throw new SmartVMError( `Invalid subnet '${subnet}': at least two usable host addresses are required`, 'INVALID_SUBNET', ); } return { networkAddress, broadcastAddress, cidr, subnetMask: this.intToIp(mask), }; } private ipToInt(ip: string): number { const octets = ip.split('.'); if (octets.length !== 4) { throw new SmartVMError(`Invalid IPv4 address '${ip}'`, 'INVALID_SUBNET'); } if (octets.some((octet) => !/^[0-9]+$/.test(octet))) { throw new SmartVMError(`Invalid IPv4 address '${ip}'`, 'INVALID_SUBNET'); } const numbers = octets.map((octet) => Number(octet)); if (numbers.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) { throw new SmartVMError(`Invalid IPv4 address '${ip}'`, 'INVALID_SUBNET'); } return ( numbers[0] * 256 ** 3 + numbers[1] * 256 ** 2 + numbers[2] * 256 + numbers[3] ) >>> 0; } private intToIp(address: number): string { return [ Math.floor(address / 256 ** 3) % 256, Math.floor(address / 256 ** 2) % 256, Math.floor(address / 256) % 256, address % 256, ].join('.'); } private cidrToMask(cidr: number): number { return (0xffffffff << (32 - cidr)) >>> 0; } private validateInterfaceName(name: string, fieldName: string): void { if (typeof name !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,14}$/.test(name)) { throw new SmartVMError( `${fieldName} '${name}' is not a valid Linux interface name`, 'INVALID_INTERFACE_NAME', ); } } private buildFirewallChainName(bridgeName: string, subnetBase: string, subnetCidr: number): string { const hash = plugins.crypto .createHash('sha256') .update(`${bridgeName}:${subnetBase}/${subnetCidr}`) .digest('hex') .slice(0, 10); return `SVMEG-${hash}`; } private getSubnetCidr(): string { return `${this.subnetBase}/${this.subnetCidr}`; } private validateIpv4Cidr(value: string, fieldName: string, errorCode = 'INVALID_FIREWALL_CONFIG'): void { const [ip, cidrText, extra] = value.split('/'); const cidr = cidrText === undefined || cidrText === '' ? 32 : Number(cidrText); if (!ip || extra !== undefined || !Number.isInteger(cidr) || cidr < 0 || cidr > 32) { throw new SmartVMError( `${fieldName} '${value}' must be an IPv4 address or CIDR with prefix length 0-32`, errorCode, ); } try { this.ipToInt(ip); } catch (err) { if (err instanceof SmartVMError) { throw new SmartVMError( `${fieldName} '${value}' must be an IPv4 address or CIDR with prefix length 0-32`, errorCode, ); } throw err; } } private normalizeCidr(value: string): string { return value.includes('/') ? value : `${value}/32`; } private validateFirewallConfig(firewall?: IFirewallConfig): void { if (!firewall || firewall.egress === undefined) { return; } const egress = firewall.egress; if (!egress || typeof egress !== 'object') { throw new SmartVMError('Firewall egress config must be an object', 'INVALID_FIREWALL_CONFIG'); } if ( egress.defaultAction !== undefined && egress.defaultAction !== 'allow' && egress.defaultAction !== 'deny' ) { throw new SmartVMError( `Invalid firewall egress defaultAction '${egress.defaultAction}'`, 'INVALID_FIREWALL_CONFIG', ); } if (egress.rules !== undefined && !Array.isArray(egress.rules)) { throw new SmartVMError('Firewall egress rules must be an array', 'INVALID_FIREWALL_CONFIG'); } for (const rule of egress.rules || []) { this.validateFirewallRule(rule); } } private validateFirewallRule(rule: IFirewallRule): void { if (!rule || typeof rule !== 'object') { throw new SmartVMError('Firewall rule must be an object', 'INVALID_FIREWALL_CONFIG'); } if (rule.action !== 'allow' && rule.action !== 'deny') { throw new SmartVMError( `Invalid firewall rule action '${rule.action}'`, 'INVALID_FIREWALL_CONFIG', ); } const protocol = rule.protocol || 'all'; if (!['all', 'tcp', 'udp', 'icmp'].includes(protocol)) { throw new SmartVMError( `Invalid firewall rule protocol '${protocol}'`, 'INVALID_FIREWALL_CONFIG', ); } if (rule.to !== undefined) { if (typeof rule.to !== 'string') { throw new SmartVMError('Firewall rule destination must be a string', 'INVALID_FIREWALL_CONFIG'); } this.validateIpv4Cidr(rule.to, 'firewall rule destination'); } if (rule.comment !== undefined && typeof rule.comment !== 'string') { throw new SmartVMError('Firewall rule comment must be a string', 'INVALID_FIREWALL_CONFIG'); } const ports = this.normalizePorts(rule.ports); if (ports.length > 0 && protocol !== 'tcp' && protocol !== 'udp') { throw new SmartVMError( 'Firewall rule ports require protocol tcp or udp', 'INVALID_FIREWALL_CONFIG', ); } } private normalizePorts(ports?: number | number[]): number[] { if (ports === undefined) { return []; } const portList = Array.isArray(ports) ? ports : [ports]; if (portList.length === 0) { throw new SmartVMError('Firewall rule ports must not be empty', 'INVALID_FIREWALL_CONFIG'); } for (const port of portList) { if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new SmartVMError( `Firewall rule port '${port}' must be an integer between 1 and 65535`, 'INVALID_FIREWALL_CONFIG', ); } } return portList; } private validateWireGuardConfig(wireguard?: TWireGuardConfig): void { if (!wireguard) { return; } const routeTable = wireguard.routeTable === undefined ? 51820 : wireguard.routeTable; if (!Number.isInteger(routeTable) || routeTable < 1 || routeTable > 4294967295) { throw new SmartVMError( `WireGuard routeTable '${routeTable}' must be an integer between 1 and 4294967295`, 'INVALID_WIREGUARD_CONFIG', ); } const wireguardOptions = wireguard as unknown as Record; const hasExistingInterface = Object.prototype.hasOwnProperty.call(wireguardOptions, 'existingInterface'); const hasManagedConfig = Object.prototype.hasOwnProperty.call(wireguardOptions, 'config'); if (hasExistingInterface && hasManagedConfig) { throw new SmartVMError( 'WireGuard config must use either existingInterface or managed config, not both', 'INVALID_WIREGUARD_CONFIG', ); } if (hasExistingInterface) { this.validateInterfaceName(wireguardOptions.existingInterface as string, 'wireguard.existingInterface'); return; } if (typeof wireguardOptions.config !== 'string') { throw new SmartVMError('WireGuard managed config requires config text', 'INVALID_WIREGUARD_CONFIG'); } const interfaceName = wireguardOptions.interfaceName === undefined ? 'svwg0' : wireguardOptions.interfaceName; this.validateInterfaceName(interfaceName as string, 'wireguard.interfaceName'); this.parseWireGuardConfig(wireguardOptions.config); } private parseWireGuardConfig(config: string): IParsedWireGuardConfig { if (!config || !config.trim()) { throw new SmartVMError('WireGuard config must not be empty', 'INVALID_WIREGUARD_CONFIG'); } const unsafeFields = new Set(['preup', 'postup', 'predown', 'postdown', 'saveconfig']); const ignoredFields = new Set(['address', 'dns', 'mtu', 'table']); const wireGuardInterfaceFields = new Set(['privatekey', 'listenport', 'fwmark']); const wireGuardPeerFields = new Set([ 'publickey', 'presharedkey', 'allowedips', 'endpoint', 'persistentkeepalive', ]); const setConfigLines: string[] = []; const addresses: string[] = []; let mtu: number | undefined; let currentSection: 'Interface' | 'Peer' | null = null; let sawPrivateKey = false; for (const rawLine of config.split(/\r?\n/)) { const trimmedLine = rawLine.trim(); if (!trimmedLine || trimmedLine.startsWith('#') || trimmedLine.startsWith(';')) { continue; } const lineWithoutComment = trimmedLine.replace(/\s+[;#].*$/, '').trim(); if (!lineWithoutComment) { continue; } const sectionMatch = lineWithoutComment.match(/^\[(Interface|Peer)\]$/i); if (sectionMatch) { currentSection = sectionMatch[1].toLowerCase() === 'interface' ? 'Interface' : 'Peer'; setConfigLines.push(`[${currentSection}]`); continue; } const keyValueMatch = lineWithoutComment.match(/^([^=]+?)\s*=\s*(.+)$/); if (!keyValueMatch || !currentSection) { throw new SmartVMError( `Invalid WireGuard config line '${rawLine.trim()}'`, 'INVALID_WIREGUARD_CONFIG', ); } const key = keyValueMatch[1].trim(); const normalizedKey = key.toLowerCase(); const value = keyValueMatch[2].trim(); if (unsafeFields.has(normalizedKey)) { throw new SmartVMError( `WireGuard config field '${key}' is not allowed because it can execute commands or mutate host state`, 'INVALID_WIREGUARD_CONFIG', ); } if (currentSection === 'Interface' && normalizedKey === 'address') { for (const address of value.split(',').map((item) => item.trim()).filter(Boolean)) { this.validateIpv4Cidr(address, 'WireGuard Address', 'INVALID_WIREGUARD_CONFIG'); addresses.push(this.normalizeCidr(address)); } continue; } if (currentSection === 'Interface' && normalizedKey === 'mtu') { const parsedMtu = Number(value); if (!Number.isInteger(parsedMtu) || parsedMtu < 576 || parsedMtu > 9000) { throw new SmartVMError( `WireGuard MTU '${value}' must be an integer between 576 and 9000`, 'INVALID_WIREGUARD_CONFIG', ); } mtu = parsedMtu; continue; } if (currentSection === 'Peer' && normalizedKey === 'allowedips') { const allowedIps = value.split(',').map((item) => item.trim()).filter(Boolean); if (allowedIps.length === 0) { throw new SmartVMError('WireGuard Peer.AllowedIPs must not be empty', 'INVALID_WIREGUARD_CONFIG'); } for (const allowedIp of allowedIps) { this.validateIpv4Cidr(allowedIp, 'WireGuard AllowedIPs', 'INVALID_WIREGUARD_CONFIG'); } setConfigLines.push(`${key} = ${allowedIps.join(', ')}`); continue; } if (ignoredFields.has(normalizedKey)) { continue; } const allowedFields = currentSection === 'Interface' ? wireGuardInterfaceFields : wireGuardPeerFields; if (!allowedFields.has(normalizedKey)) { throw new SmartVMError( `Unsupported WireGuard ${currentSection} field '${key}'`, 'INVALID_WIREGUARD_CONFIG', ); } if (currentSection === 'Interface' && normalizedKey === 'privatekey') { sawPrivateKey = true; } setConfigLines.push(`${key} = ${value}`); } if (!sawPrivateKey) { throw new SmartVMError('WireGuard config requires Interface.PrivateKey', 'INVALID_WIREGUARD_CONFIG'); } if (addresses.length === 0) { throw new SmartVMError('WireGuard config requires at least one IPv4 Interface.Address', 'INVALID_WIREGUARD_CONFIG'); } return { setConfig: `${setConfigLines.join('\n')}\n`, addresses, mtu, }; } /** * Allocate the next available IP address in the subnet. */ public allocateIp(): string { if (this.nextIpAddress > this.lastUsableIpAddress) { throw new SmartVMError( `Subnet ${this.subnetBase}/${this.subnetCidr} has no available guest IP addresses`, 'IP_EXHAUSTED', ); } const ip = this.intToIp(this.nextIpAddress); this.nextIpAddress++; return ip; } /** * Generate a deterministic locally-administered MAC address. */ public generateMac(vmId: string, ifaceId: string): string { // Create a simple hash from vmId + ifaceId for deterministic MAC generation const input = `${vmId}:${ifaceId}`; let hash = 0; for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } // Ensure hash is positive const h = Math.abs(hash); // Generate MAC octets from hash, using locally-administered prefix (02:xx:xx:xx:xx:xx) const mac = [ 0x02, (h >> 0) & 0xff, (h >> 8) & 0xff, (h >> 16) & 0xff, (h >> 24) & 0xff, ((h >> 4) ^ (h >> 12)) & 0xff, ]; return mac.map((b) => b.toString(16).padStart(2, '0')).join(':'); } /** * Generate a TAP device name that fits within IFNAMSIZ (15 chars). * Format: sv<4charVmId> */ public generateTapName(vmId: string, ifaceId: string): string { const vmPart = vmId.replace(/-/g, '').substring(0, 4); const ifacePart = ifaceId.substring(0, 6); const tapName = `sv${vmPart}${ifacePart}`; // Ensure max 15 chars (Linux IFNAMSIZ) return tapName.substring(0, 15); } private async run(command: string, args: string[]): Promise { return this.shell.execSpawn(command, args, { silent: true }); } private async runChecked(command: string, args: string[]): Promise { const result = await this.run(command, args); if (result.exitCode !== 0) { const output = (result.stderr || result.stdout || '').trim(); throw new Error(`${command} ${args.join(' ')} exited with code ${result.exitCode}${output ? `: ${output}` : ''}`); } return result; } private async getDefaultRouteInterface(): Promise { if (this.defaultRouteInterface) { return this.defaultRouteInterface; } const result = await this.runChecked('ip', ['route', 'show', 'default']); const match = result.stdout.match(/\bdev\s+([^\s]+)/); if (!match) { throw new Error('Could not determine default route interface'); } const iface = match[1]; this.validateInterfaceName(iface, 'default route interface'); this.defaultRouteInterface = iface; return iface; } private getSharedMemoryTempDir(): string { try { if (plugins.fs.existsSync('/dev/shm') && plugins.fs.statSync('/dev/shm').isDirectory()) { return '/dev/shm'; } } catch { // Fall through to os.tmpdir(). } return plugins.os.tmpdir(); } private async configureWireGuardEgress(): Promise { if (!this.wireguard || this.wireguard.routeAllVmTraffic === false) { return this.getDefaultRouteInterface(); } try { const iface = await this.ensureWireGuardInterface(); const routeTable = this.wireguard.routeTable === undefined ? 51820 : this.wireguard.routeTable; this.wireGuardInterface = iface; this.wireGuardRouteTable = routeTable; this.wireGuardRouteAdded = false; this.wireGuardIpRuleAdded = false; const routeResult = await this.run('ip', ['route', 'show', 'table', String(routeTable), 'default']); const existingDefaultRoutes = routeResult.stdout .split('\n') .map((line) => line.trim()) .filter(Boolean); if (existingDefaultRoutes.some((line) => !line.includes(` dev ${iface} `) && !line.endsWith(` dev ${iface}`))) { throw new SmartVMError( `WireGuard route table ${routeTable} already has a default route not using ${iface}`, 'WIREGUARD_SETUP_FAILED', ); } if (existingDefaultRoutes.length === 0) { await this.runChecked('ip', [ 'route', 'add', 'default', 'dev', iface, 'table', String(routeTable), ]); this.wireGuardRouteAdded = true; } this.wireGuardRouteConfigured = true; if (!await this.hasWireGuardIpRule(routeTable)) { await this.runChecked('ip', [ 'rule', 'add', 'from', this.getSubnetCidr(), 'table', String(routeTable), ]); this.wireGuardIpRuleAdded = true; } return iface; } catch (err) { if (err instanceof SmartVMError) { throw err; } const message = err instanceof Error ? err.message : String(err); throw new SmartVMError(`Failed to configure WireGuard egress: ${message}`, 'WIREGUARD_SETUP_FAILED'); } } private async hasWireGuardIpRule(routeTable: number): Promise { const result = await this.run('ip', ['rule', 'show']); return result.stdout .split('\n') .some((line) => line.includes(`from ${this.getSubnetCidr()}`) && line.includes(`lookup ${routeTable}`)); } private async ensureWireGuardInterface(): Promise { if (!this.wireguard) { throw new Error('WireGuard is not configured'); } if ('existingInterface' in this.wireguard) { await this.runChecked('ip', ['link', 'show', this.wireguard.existingInterface]); this.wireGuardManaged = false; return this.wireguard.existingInterface; } const iface = this.wireguard.interfaceName || 'svwg0'; const existingInterface = await this.run('ip', ['link', 'show', iface]); if (existingInterface.exitCode === 0) { throw new SmartVMError( `Managed WireGuard interface '${iface}' already exists; use existingInterface to route through it`, 'WIREGUARD_SETUP_FAILED', ); } const parsedConfig = this.parseWireGuardConfig(this.wireguard.config); await this.runChecked('ip', ['link', 'add', 'dev', iface, 'type', 'wireguard']); this.wireGuardManaged = true; this.wireGuardInterface = iface; const tempDir = await plugins.fs.promises.mkdtemp( plugins.path.join(this.getSharedMemoryTempDir(), `smartvm-wg-${iface}-`), ); const tempConfigPath = plugins.path.join(tempDir, 'wg.conf'); try { await plugins.fs.promises.writeFile(tempConfigPath, parsedConfig.setConfig, { mode: 0o600 }); await this.runChecked('wg', ['setconf', iface, tempConfigPath]); } finally { await plugins.fs.promises.rm(tempDir, { recursive: true, force: true }); } for (const address of parsedConfig.addresses) { await this.runChecked('ip', ['addr', 'add', address, 'dev', iface]); } if (parsedConfig.mtu !== undefined) { await this.runChecked('ip', ['link', 'set', 'mtu', String(parsedConfig.mtu), 'dev', iface]); } await this.runChecked('ip', ['link', 'set', iface, 'up']); return iface; } private shouldApplyFailClosed(): boolean { return Boolean( this.wireguard && this.wireguard.routeAllVmTraffic !== false && this.wireguard.failClosed !== false && this.wireGuardInterface, ); } private async setupNat(egressInterface: string): Promise { this.natRuleAdded = false; const ruleArgs = [ '-s', this.getSubnetCidr(), '-o', egressInterface, '-j', 'MASQUERADE', ]; const checkResult = await this.run('iptables', ['-t', 'nat', '-C', 'POSTROUTING', ...ruleArgs]); if (checkResult.exitCode !== 0) { await this.runChecked('iptables', ['-t', 'nat', '-A', 'POSTROUTING', ...ruleArgs]); this.natRuleAdded = true; } this.natInterface = egressInterface; this.natConfigured = true; } private async setupEgressFirewall(egressInterface: string): Promise { const egress = this.firewall?.egress; const shouldSetupFirewall = Boolean(egress || this.shouldApplyFailClosed()); if (!shouldSetupFirewall) { return; } await this.ensureIptablesChain('filter', this.firewallChainName); this.firewallConfigured = true; await this.runChecked('iptables', ['-t', 'filter', '-F', this.firewallChainName]); await this.ensureIptablesRule('filter', 'FORWARD', ['-s', this.getSubnetCidr(), '-j', this.firewallChainName]); await this.runChecked('iptables', [ '-t', 'filter', '-A', this.firewallChainName, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT', ]); if (this.shouldApplyFailClosed()) { await this.runChecked('iptables', [ '-t', 'filter', '-A', this.firewallChainName, '!', '-o', egressInterface, '-j', 'DROP', ]); } for (const rule of egress?.rules || []) { for (const ruleArgs of this.buildFirewallRuleArgs(rule)) { await this.runChecked('iptables', ['-t', 'filter', '-A', this.firewallChainName, ...ruleArgs]); } } const defaultAction = egress?.defaultAction || 'allow'; await this.runChecked('iptables', [ '-t', 'filter', '-A', this.firewallChainName, '-j', FIREWALL_ACTION_TO_IPTABLES_TARGET[defaultAction], ]); } private buildFirewallRuleArgs(rule: IFirewallRule): string[][] { const baseArgs: string[] = []; const protocol = rule.protocol || 'all'; if (rule.to !== undefined) { baseArgs.push('-d', this.normalizeCidr(rule.to)); } if (protocol !== 'all') { baseArgs.push('-p', protocol); } if (rule.comment) { baseArgs.push('-m', 'comment', '--comment', rule.comment.slice(0, 240)); } const target = FIREWALL_ACTION_TO_IPTABLES_TARGET[rule.action]; const ports = this.normalizePorts(rule.ports); if (ports.length === 0) { return [[...baseArgs, '-j', target]]; } return ports.map((port) => [...baseArgs, '--dport', String(port), '-j', target]); } private async ensureIptablesChain(table: string, chain: string): Promise { const checkResult = await this.run('iptables', ['-t', table, '-S', chain]); if (checkResult.exitCode !== 0) { await this.runChecked('iptables', ['-t', table, '-N', chain]); } } private async ensureIptablesRule(table: string, chain: string, ruleArgs: string[]): Promise { const checkResult = await this.run('iptables', ['-t', table, '-C', chain, ...ruleArgs]); if (checkResult.exitCode !== 0) { await this.runChecked('iptables', ['-t', table, '-A', chain, ...ruleArgs]); } } private async deleteIptablesRule(table: string, chain: string, ruleArgs: string[]): Promise { await this.run('iptables', ['-t', table, '-D', chain, ...ruleArgs]); } /** * Ensure the Linux bridge is created and configured. */ public async ensureBridge(): Promise { if (this.bridgeCreated) return; let createdBridge = false; try { // Check if bridge already exists const result = await this.run('ip', ['link', 'show', this.bridgeName]); if (result.exitCode !== 0) { // Create bridge await this.runChecked('ip', ['link', 'add', this.bridgeName, 'type', 'bridge']); createdBridge = true; await this.runChecked('ip', ['addr', 'add', `${this.gatewayIp}/${this.subnetCidr}`, 'dev', this.bridgeName]); await this.runChecked('ip', ['link', 'set', this.bridgeName, 'up']); } // Enable IP forwarding await this.runChecked('sysctl', ['-w', 'net.ipv4.ip_forward=1']); const egressInterface = await this.configureWireGuardEgress(); await this.setupEgressFirewall(egressInterface); await this.setupNat(egressInterface); this.bridgeCreated = true; } catch (err) { try { await this.cleanupEgressFirewall(); await this.cleanupNat(); await this.cleanupWireGuardEgress(); if (createdBridge) { await this.run('ip', ['link', 'set', this.bridgeName, 'down']); await this.run('ip', ['link', 'del', this.bridgeName]); } } catch { // Preserve the original setup error. } if (err instanceof SmartVMError) { throw err; } const message = err instanceof Error ? err.message : String(err); throw new SmartVMError( `Failed to set up network bridge: ${message}`, 'BRIDGE_SETUP_FAILED', ); } } /** * Create a TAP device for a VM and attach it to the bridge. */ public async createTapDevice(vmId: string, ifaceId: string): Promise { await this.ensureBridge(); const tapName = this.generateTapName(vmId, ifaceId); this.validateInterfaceName(tapName, 'tapName'); const guestIp = this.allocateIp(); const mac = this.generateMac(vmId, ifaceId); let tapCreated = false; try { // Create TAP device await this.runChecked('ip', ['tuntap', 'add', 'dev', tapName, 'mode', 'tap']); tapCreated = true; // Attach to bridge await this.runChecked('ip', ['link', 'set', tapName, 'master', this.bridgeName]); // Bring TAP device up await this.runChecked('ip', ['link', 'set', tapName, 'up']); const tap: ITapDevice = { tapName, guestIp, gatewayIp: this.gatewayIp, subnetMask: this.subnetMask, mac, }; this.activeTaps.set(tapName, tap); return tap; } catch (err) { if (tapCreated) { await this.removeTapDevice(tapName); } const message = err instanceof Error ? err.message : String(err); throw new SmartVMError( `Failed to create TAP device ${tapName}: ${message}`, 'TAP_CREATE_FAILED', ); } } /** * Remove a TAP device and free its resources. */ public async removeTapDevice(tapName: string): Promise { this.validateInterfaceName(tapName, 'tapName'); try { await this.run('ip', ['link', 'del', tapName]); this.activeTaps.delete(tapName); } catch { // Device may already be gone } } /** * Generate kernel boot args for guest networking. * Returns the `ip=` parameter for the kernel command line. */ public getGuestNetworkBootArgs(tap: ITapDevice): string { // Format: ip=::::::off return `ip=${tap.guestIp}::${tap.gatewayIp}:${tap.subnetMask}::eth0:off`; } /** * Get all active TAP devices. */ public getActiveTaps(): ITapDevice[] { return Array.from(this.activeTaps.values()); } /** * Clean up all TAP devices and the bridge. */ public async cleanup(): Promise { // Remove all TAP devices for (const tapName of Array.from(this.activeTaps.keys())) { await this.removeTapDevice(tapName); } await this.cleanupEgressFirewall(); await this.cleanupNat(); await this.cleanupWireGuardEgress(); // Remove bridge if we created it if (this.bridgeCreated) { try { await this.run('ip', ['link', 'set', this.bridgeName, 'down']); await this.run('ip', ['link', 'del', this.bridgeName]); } catch { // Bridge may already be gone } this.bridgeCreated = false; } } private async cleanupEgressFirewall(): Promise { if (!this.firewallConfigured) { return; } await this.deleteIptablesRule('filter', 'FORWARD', [ '-s', this.getSubnetCidr(), '-j', this.firewallChainName, ]); await this.run('iptables', ['-t', 'filter', '-F', this.firewallChainName]); await this.run('iptables', ['-t', 'filter', '-X', this.firewallChainName]); this.firewallConfigured = false; } private async cleanupNat(): Promise { if (!this.natConfigured || !this.natInterface) { return; } if (this.natRuleAdded) { await this.deleteIptablesRule('nat', 'POSTROUTING', [ '-s', this.getSubnetCidr(), '-o', this.natInterface, '-j', 'MASQUERADE', ]); } this.natInterface = null; this.natConfigured = false; this.natRuleAdded = false; } private async cleanupWireGuardEgress(): Promise { if (this.wireGuardRouteConfigured && this.wireGuardRouteTable !== null) { if (this.wireGuardIpRuleAdded) { await this.run('ip', [ 'rule', 'del', 'from', this.getSubnetCidr(), 'table', String(this.wireGuardRouteTable), ]); } if (this.wireGuardRouteAdded && this.wireGuardInterface) { await this.run('ip', [ 'route', 'del', 'default', 'dev', this.wireGuardInterface, 'table', String(this.wireGuardRouteTable), ]); } } if (this.wireGuardManaged && this.wireGuardInterface) { await this.run('ip', ['link', 'del', this.wireGuardInterface]); } this.wireGuardRouteConfigured = false; this.wireGuardRouteAdded = false; this.wireGuardIpRuleAdded = false; this.wireGuardRouteTable = null; this.wireGuardManaged = false; this.wireGuardInterface = null; } }