Files
smartvm/ts/classes.networkmanager.ts

238 lines
7.2 KiB
TypeScript
Raw Permalink Normal View History

2026-02-08 21:47:33 +00:00
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<string, ITapDevice> = new Map();
private bridgeCreated: boolean = false;
private shell: InstanceType<typeof plugins.smartshell.Smartshell>;
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><ifaceId truncated>
*/
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<void> {
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<ITapDevice> {
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<void> {
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=<client-ip>:<server-ip>:<gw-ip>:<netmask>::<device>: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<void> {
// 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;
}
}
}