#!/usr/bin/env bash # test-firewall-scenarios.sh — 3-VM QEMU topology for firewall validation. # # Topology: # ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ # │ VM-A │ │ Router-VM │ │ VM-B │ # │ (internal) │◄───►│ 2 NICs │◄───►│ (external) │ # │ 10.0.0.2/24 │ │ eth0: 10.0.0.1 │ │ eth1: dyn │ # └─────────────┘ │ eth1: 10.0.2.100│ └─────────────┘ # └─────────────┘ # # Router-VM runs redbear-firewall-check to exercise the netfilter control # plane. pcaps are captured on each NIC via QEMU filter-dump. # # Usage: # ./test-firewall-scenarios.sh [--check] [REDBEAR_CONFIG] # # REDBEAR_CONFIG defaults to redbear-mini (text-only, smaller ISO). # --check: run in automated check mode using expect. # # Artifacts are saved to artifacts/firewall-/. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" ISO_CONFIG="${REDBEAR_CONFIG:-redbear-mini}" ARCH="${ARCH:-$(uname -m)}" TIMESTAMP="$(date +%Y%m%d-%H%M%S)" ARTIFACT_DIR="$ROOT_DIR/artifacts/firewall-$TIMESTAMP" declare -a vm_pids=() declare -a sock_paths=() cleanup_done=0 # ────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────── find_uefi_firmware() { local candidates=( "/usr/share/ovmf/x64/OVMF.4m.fd" "/usr/share/OVMF/x64/OVMF.4m.fd" "/usr/share/ovmf/x64/OVMF_CODE.4m.fd" "/usr/share/OVMF/x64/OVMF_CODE.4m.fd" "/usr/share/ovmf/OVMF.fd" "/usr/share/OVMF/OVMF_CODE.fd" "/usr/share/qemu/edk2-x86_64-code.fd" ) for path in "${candidates[@]}"; do if [[ -f "$path" ]]; then printf '%s\n' "$path" return 0 fi done return 1 } safe_kill_vm() { local pid="$1" if kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true # Wait up to 5 seconds for graceful shutdown local waited=0 while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 10 ]]; do sleep 0.5 waited=$((waited + 1)) done # Force kill if still alive kill -9 "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi } cleanup() { if [[ $cleanup_done -eq 1 ]]; then return fi cleanup_done=1 echo "=== Cleaning up VMs ===" for pid in "${vm_pids[@]}"; do safe_kill_vm "$pid" done for sock in "${sock_paths[@]}"; do rm -f "$sock" done echo "VMs shut down." echo "Artifacts: $ARTIFACT_DIR" } trap cleanup EXIT INT TERM usage() { cat <<'USAGE' Usage: test-firewall-scenarios.sh [--check] [REDBEAR_CONFIG] 3-VM QEMU topology for firewall validation. REDBEAR_CONFIG Config to build (default: redbear-mini) --check Automated mode — runs redbear-firewall-check inside Router-VM via expect and validates all 6 scenarios pass. --help, -h Show this message Examples: # Build and launch 3-VM topology interactively: ./local/scripts/test-firewall-scenarios.sh redbear-mini # Run automated check (builds if needed): ./local/scripts/test-firewall-scenarios.sh --check redbear-mini USAGE } # ────────────────────────────────────────────────────────────────────────── # Parse arguments # ────────────────────────────────────────────────────────────────────────── check_mode=0 for arg in "$@"; do case "$arg" in --help|-h|help) usage exit 0 ;; --check) check_mode=1 ;; -*) echo "ERROR: unknown flag: $arg" >&2 usage >&2 exit 1 ;; *) ISO_CONFIG="$arg" ;; esac done # ────────────────────────────────────────────────────────────────────────── # Build prerequisites # ────────────────────────────────────────────────────────────────────────── ISO_PATH="$ROOT_DIR/build/$ARCH/$ISO_CONFIG.iso" BUILD_SCRIPT="$ROOT_DIR/local/scripts/build-redbear.sh" if [[ ! -f "$ISO_PATH" ]]; then echo "=== ISO not found, building $ISO_CONFIG ===" "$BUILD_SCRIPT" "$ISO_CONFIG" || { echo "ERROR: build failed" >&2 exit 1 } fi firmware="$(find_uefi_firmware)" || { echo "ERROR: no usable x86_64 UEFI firmware found" >&2 exit 1 } echo "UEFI firmware: $firmware" echo "ISO: $ISO_PATH" # ────────────────────────────────────────────────────────────────────────── # Create artifact directory # ────────────────────────────────────────────────────────────────────────── mkdir -p "$ARTIFACT_DIR" echo "Artifacts will be saved to: $ARTIFACT_DIR" # ────────────────────────────────────────────────────────────────────────── # Create AF_UNIX sockets for inter-VM networking # ────────────────────────────────────────────────────────────────────────── SOCK_A="$ARTIFACT_DIR/vm-a-net0.sock" SOCK_B="$ARTIFACT_DIR/vm-b-net0.sock" SOCK_RTR_INT="$ARTIFACT_DIR/router-int.sock" # Router eth0 (internal) SOCK_RTR_EXT="$ARTIFACT_DIR/router-ext.sock" # Router eth1 (external) # Cleanup any stale sockets rm -f "$SOCK_A" "$SOCK_B" "$SOCK_RTR_INT" "$SOCK_RTR_EXT" sock_paths=("$SOCK_A" "$SOCK_B" "$SOCK_RTR_INT" "$SOCK_RTR_EXT") # ────────────────────────────────────────────────────────────────────────── # Router-VM: two NICs (eth0=internal subnet, eth1=external) # ────────────────────────────────────────────────────────────────────────── ROUTER_ISO="$ISO_PATH" ROUTER_PCAP_INT="$ARTIFACT_DIR/router-eth0.pcap" ROUTER_PCAP_EXT="$ARTIFACT_DIR/router-eth1.pcap" ROUTER_QMP="$ARTIFACT_DIR/router-qmp.sock" # Check if expect script is available for automated mode EXPECT_SCRIPT="$ROOT_DIR/local/scripts/qemu-login-expect.py" if [[ "$check_mode" -eq 1 ]]; then if [[ ! -f "$EXPECT_SCRIPT" ]]; then echo "WARNING: expect script not found at $EXPECT_SCRIPT — falling back to interactive mode" >&2 check_mode=0 fi if ! command -v python3 &>/dev/null; then echo "WARNING: python3 not found — falling back to interactive mode" >&2 check_mode=0 fi fi echo "" echo "=== Topology ===" echo " VM-A: 10.0.0.2/24 (internal subnet)" echo " Router-VM: 10.0.0.1/24 (eth0, internal), 10.0.2.100/24 (eth1, external)" echo " VM-B: 10.0.2.200/24 (external subnet)" echo "" echo "=== Launching VMs ===" # ────────────────────────────────────────────────────────────────────────── # Launch Router-VM first (firewall host, runs redbear-firewall-check) # ────────────────────────────────────────────────────────────────────────── echo "Starting Router-VM..." if [[ "$check_mode" -eq 1 ]]; then python3 "$EXPECT_SCRIPT" \ --timeout 300 \ --log "$ARTIFACT_DIR/router-boot.log" \ --step "expect:login:" \ --step "send:root" \ --step "expect:assword:" \ --step "send:password" \ --step "expect:Type 'help' for available commands." \ --step "send:redbear-firewall-check" \ --step "expect:Firewall Validation Harness" \ --step "expect:SCENARIO_A=pass" \ --step "expect:SCENARIO_B=pass" \ --step "expect:SCENARIO_C=pass" \ --step "expect:SCENARIO_D=pass" \ --step "expect:SCENARIO_E=pass" \ --step "expect:SCENARIO_F=pass" \ --step "expect:Summary" \ --pass_marker "SCENARIO_F=pass" \ --step "send:shutdown" \ -- \ qemu-system-x86_64 \ -name "router-vm" \ -smp 2 \ -m 512 \ -bios "$firmware" \ -machine q35 \ -nographic \ -cdrom "$ROUTER_ISO" \ -netdev socket,id=net0,listen=:0,path="$SOCK_RTR_INT" \ -device virtio-net,netdev=net0,mac=52:54:00:12:34:01 \ -object filter-dump,id=f0,netdev=net0,file="$ROUTER_PCAP_INT" \ -netdev socket,id=net1,listen=:0,path="$SOCK_RTR_EXT" \ -device virtio-net,netdev=net1,mac=52:54:00:12:34:02 \ -object filter-dump,id=f1,netdev=net1,file="$ROUTER_PCAP_EXT" \ -qmp unix:"$ROUTER_QMP",server,nowait \ -enable-kvm -cpu host & router_pid=$! else qemu-system-x86_64 \ -name "router-vm" \ -smp 2 \ -m 512 \ -bios "$firmware" \ -machine q35 \ -nographic \ -cdrom "$ROUTER_ISO" \ -netdev socket,id=net0,listen=:0,path="$SOCK_RTR_INT" \ -device virtio-net,netdev=net0,mac=52:54:00:12:34:01 \ -object filter-dump,id=f0,netdev=net0,file="$ROUTER_PCAP_INT" \ -netdev socket,id=net1,listen=:0,path="$SOCK_RTR_EXT" \ -device virtio-net,netdev=net1,mac=52:54:00:12:34:02 \ -object filter-dump,id=f1,netdev=net1,file="$ROUTER_PCAP_EXT" \ -qmp unix:"$ROUTER_QMP",server,nowait \ -enable-kvm -cpu host & router_pid=$! fi vm_pids+=("$router_pid") echo " Router-VM PID: $router_pid" echo " Router pcaps: $ROUTER_PCAP_INT, $ROUTER_PCAP_EXT" # Wait a moment for router sockets to be created before connecting VMs sleep 2 for sock in "$SOCK_RTR_INT" "$SOCK_RTR_EXT"; do waited=0 while [[ ! -S "$sock" ]] && [[ $waited -lt 30 ]]; do sleep 1 waited=$((waited + 1)) done if [[ ! -S "$sock" ]]; then echo "WARNING: socket $sock not created after 30s — continuing anyway" >&2 fi done # ────────────────────────────────────────────────────────────────────────── # Launch VM-A (internal subnet peer) # ────────────────────────────────────────────────────────────────────────── echo "Starting VM-A (internal subnet)..." VM_A_PCAP="$ARTIFACT_DIR/vm-a-net0.pcap" qemu-system-x86_64 \ -name "vm-a" \ -smp 1 \ -m 256 \ -bios "$firmware" \ -machine q35 \ -nographic \ -cdrom "$ISO_PATH" \ -netdev socket,id=net0,connect=:0,path="$SOCK_RTR_INT" \ -device virtio-net,netdev=net0,mac=52:54:00:12:34:0A \ -object filter-dump,id=fa,netdev=net0,file="$VM_A_PCAP" \ -enable-kvm -cpu host & vm_a_pid=$! vm_pids+=("$vm_a_pid") echo " VM-A PID: $vm_a_pid" echo " VM-A pcap: $VM_A_PCAP" # ────────────────────────────────────────────────────────────────────────── # Launch VM-B (external subnet peer) # ────────────────────────────────────────────────────────────────────────── echo "Starting VM-B (external subnet)..." VM_B_PCAP="$ARTIFACT_DIR/vm-b-net0.pcap" qemu-system-x86_64 \ -name "vm-b" \ -smp 1 \ -m 256 \ -bios "$firmware" \ -machine q35 \ -nographic \ -cdrom "$ISO_PATH" \ -netdev socket,id=net0,connect=:0,path="$SOCK_RTR_EXT" \ -device virtio-net,netdev=net0,mac=52:54:00:12:34:0B \ -object filter-dump,id=fb,netdev=net0,file="$VM_B_PCAP" \ -enable-kvm -cpu host & vm_b_pid=$! vm_pids+=("$vm_b_pid") echo " VM-B PID: $vm_b_pid" echo " VM-B pcap: $VM_B_PCAP" echo "" echo "=== All 3 VMs launched ===" echo "PIDs: router=$router_pid, vm-a=$vm_a_pid, vm-b=$vm_b_pid" echo "" # ────────────────────────────────────────────────────────────────────────── # Summary file # ────────────────────────────────────────────────────────────────────────── SUMMARY="$ARTIFACT_DIR/summary.txt" cat > "$SUMMARY" </dev/null; then echo "SCENARIO_${label}: ✅ pass" else echo "SCENARIO_${label}: ❌ not found in output" all_pass=0 fi done if [[ $all_pass -eq 1 ]]; then echo "" echo "=== All 6 firewall scenarios PASSED ===" exit 0 else echo "" echo "=== Some firewall scenarios FAILED or did not produce output ===" echo "Full log: $ROUTER_LOG" exit 1 fi else echo "ERROR: Router boot log not found at $ROUTER_LOG" exit 1 fi else echo "" echo "=== VMs are running — press Ctrl+C to stop ===" echo "Log into Router-VM and run: redbear-firewall-check" echo "" # Wait for any VM to exit, or for Ctrl+C wait -n "${vm_pids[@]}" 2>/dev/null || true fi