import * as plugins from './plugins.js'; import type { INetworkManagerOptions, ITapDevice } from './interfaces/index.js'; import { SmartVMError } from './interfaces/index.js'; /** * 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 nextIpOctet: number; private activeTaps: Map = new Map(); private bridgeCreated: boolean = false; private shell: InstanceType; constructor(options: INetworkManagerOptions = {}) { this.bridgeName = options.bridgeName || 'svbr0'; const subnet = options.subnet || '172.30.0.0/24'; // Parse the subnet const [baseIp, cidrStr] = subnet.split('/'); this.subnetBase = baseIp; this.subnetCidr = parseInt(cidrStr, 10); this.subnetMask = this.cidrToSubnetMask(this.subnetCidr); // Gateway is .1 in the subnet const parts = this.subnetBase.split('.').map(Number); parts[3] = 1; this.gatewayIp = parts.join('.'); // VMs start at .2 this.nextIpOctet = 2; this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); } /** * Convert a CIDR prefix length to a dotted-decimal subnet mask. */ private cidrToSubnetMask(cidr: number): string { const mask = (0xffffffff << (32 - cidr)) >>> 0; return [ (mask >>> 24) & 0xff, (mask >>> 16) & 0xff, (mask >>> 8) & 0xff, mask & 0xff, ].join('.'); } /** * Allocate the next available IP address in the subnet. */ public allocateIp(): string { const parts = this.subnetBase.split('.').map(Number); parts[3] = this.nextIpOctet; this.nextIpOctet++; return parts.join('.'); } /** * 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); } /** * Ensure the Linux bridge is created and configured. */ public async ensureBridge(): Promise { if (this.bridgeCreated) return; try { // Check if bridge already exists const result = await this.shell.exec(`ip link show ${this.bridgeName} 2>/dev/null`); if (result.exitCode !== 0) { // Create bridge await this.shell.exec(`ip link add ${this.bridgeName} type bridge`); await this.shell.exec(`ip addr add ${this.gatewayIp}/${this.subnetCidr} dev ${this.bridgeName}`); await this.shell.exec(`ip link set ${this.bridgeName} up`); } // Enable IP forwarding await this.shell.exec('sysctl -w net.ipv4.ip_forward=1'); // Set up NAT masquerade (idempotent with -C check) const checkResult = await this.shell.exec( `iptables -t nat -C POSTROUTING -s ${this.subnetBase}/${this.subnetCidr} -o $(ip route | grep default | awk '{print $5}' | head -1) -j MASQUERADE 2>/dev/null`, ); if (checkResult.exitCode !== 0) { await this.shell.exec( `iptables -t nat -A POSTROUTING -s ${this.subnetBase}/${this.subnetCidr} -o $(ip route | grep default | awk '{print $5}' | head -1) -j MASQUERADE`, ); } this.bridgeCreated = true; } catch (err) { throw new SmartVMError( `Failed to set up network bridge: ${err.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); const guestIp = this.allocateIp(); const mac = this.generateMac(vmId, ifaceId); try { // Create TAP device await this.shell.exec(`ip tuntap add dev ${tapName} mode tap`); // Attach to bridge await this.shell.exec(`ip link set ${tapName} master ${this.bridgeName}`); // Bring TAP device up await this.shell.exec(`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) { throw new SmartVMError( `Failed to create TAP device ${tapName}: ${err.message}`, 'TAP_CREATE_FAILED', ); } } /** * Remove a TAP device and free its resources. */ public async removeTapDevice(tapName: string): Promise { try { await this.shell.exec(`ip link del ${tapName} 2>/dev/null`); 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 this.activeTaps.keys()) { await this.removeTapDevice(tapName); } // Remove bridge if we created it if (this.bridgeCreated) { try { await this.shell.exec(`ip link set ${this.bridgeName} down 2>/dev/null`); await this.shell.exec(`ip link del ${this.bridgeName} 2>/dev/null`); } catch { // Bridge may already be gone } // Remove NAT rule try { await this.shell.exec( `iptables -t nat -D POSTROUTING -s ${this.subnetBase}/${this.subnetCidr} -o $(ip route | grep default | awk '{print $5}' | head -1) -j MASQUERADE 2>/dev/null`, ); } catch { // Rule may not exist } this.bridgeCreated = false; } } }