// 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`); return; } // 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 */ } // Libvirt creates nftables rules that reject traffic on the bridge. // DHCP works (dnsmasq uses raw sockets) but TFTP/HTTP from VM->host gets blocked. // Delete the reject rules so VM traffic can reach the bastion. try { // Delete the reject rules that libvirt added for our bridge. // We find and delete each rule by its handle number. const deleteRejectRules = (chain: string): void => { const output = run(`nft -a list chain inet libvirt ${chain} 2>/dev/null || true`); const lines = output.split("\n"); for (const line of lines) { if (line.includes(PXE_BRIDGE) && line.includes("reject")) { const handleMatch = line.match(/# handle (\d+)/); if (handleMatch) { run(`nft delete rule inet libvirt ${chain} handle ${handleMatch[1]}`); } } } }; deleteRejectRules("guest_input"); deleteRejectRules("guest_output"); log(`Removed nftables reject rules for ${PXE_BRIDGE}`); } catch { log(`Could not update nftables rules (may need manual firewall config)`); } log(`Network ${PXE_NETWORK_NAME} created and active`); } /** 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); }