// Libvirt network for PXE boot testing. // Unlike the regular test network, this one has NO DHCP — // the bastion provides full DHCP + PXE on this network. import { execSync, spawnSync } from "node:child_process"; import { writeFileSync, unlinkSync } from "node:fs"; import { log } from "./libvirt.js"; export const PXE_NETWORK_NAME = "lab-pxe-test"; export const PXE_BRIDGE = "virbr-pxe"; export const PXE_SUBNET = "192.168.251"; export const PXE_GATEWAY = `${PXE_SUBNET}.1`; const IS_ROOT = process.getuid?.() === 0; function run(cmd: string): string { const full = IS_ROOT ? cmd : `sudo ${cmd}`; return execSync(full, { encoding: "utf-8", stdio: "pipe" }); } function virsh(...args: string[]): { status: number; stdout: string } { const cmd = IS_ROOT ? "virsh" : "sudo"; const finalArgs = IS_ROOT ? args : ["virsh", ...args]; const result = spawnSync(cmd, finalArgs, { encoding: "utf-8", stdio: "pipe" }); return { status: result.status ?? 1, stdout: result.stdout ?? "" }; } // No section — bastion dnsmasq provides full DHCP + PXE const NETWORK_XML = ` ${PXE_NETWORK_NAME} `; /** Ensure the PXE test network exists and is active (no DHCP). */ export function ensurePxeNetwork(): void { const result = virsh("net-info", PXE_NETWORK_NAME); if (result.status === 0 && result.stdout.includes("Active: yes")) { log(`Network ${PXE_NETWORK_NAME} already active`); } else { // Destroy existing if present but inactive if (result.status === 0) { virsh("net-destroy", PXE_NETWORK_NAME); virsh("net-undefine", PXE_NETWORK_NAME); } const xmlPath = "/tmp/lab-pxe-test-network.xml"; writeFileSync(xmlPath, NETWORK_XML); log(`Creating PXE libvirt network: ${PXE_NETWORK_NAME} (${PXE_SUBNET}.0/24, no DHCP)`); run(`virsh net-define "${xmlPath}"`); run(`virsh net-start "${PXE_NETWORK_NAME}"`); try { unlinkSync(xmlPath); } catch { /* ignore */ } log(`Network ${PXE_NETWORK_NAME} created and active`); } // Libvirt adds nftables reject rules for NAT networks that block host→VM SSH. // Delete them now and after every VM reboot (libvirt recreates them). deleteNftablesRejectRules(); } /** Delete libvirt's nftables reject rules for our bridge so host→VM traffic works. * Must be called after every VM start/restart — libvirt recreates them. */ export function deleteNftablesRejectRules(): void { // libvirt uses "ip libvirt_network" table (not "inet libvirt") const tables = ["ip libvirt_network", "ip6 libvirt_network", "inet libvirt"]; for (const table of tables) { try { for (const chain of ["guest_input", "guest_output"]) { const output = run(`nft -a list chain ${table} ${chain} 2>/dev/null || true`); for (const line of output.split("\n")) { if (line.includes(PXE_BRIDGE) && line.includes("reject")) { const handleMatch = line.match(/# handle (\d+)/); if (handleMatch) { run(`nft delete rule ${table} ${chain} handle ${handleMatch[1]}`); } } } } } catch { /* table may not exist */ } } } /** Destroy the PXE test network. */ export function destroyPxeNetwork(): void { log(`Destroying PXE network: ${PXE_NETWORK_NAME}`); // nftables rules are cleaned up when the network is destroyed (libvirt removes them) virsh("net-destroy", PXE_NETWORK_NAME); virsh("net-undefine", PXE_NETWORK_NAME); }