initial
This commit is contained in:
237
ts/classes.networkmanager.ts
Normal file
237
ts/classes.networkmanager.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user