From 614d1bd7099d334613a83ceb296ddaa51a826e4b Mon Sep 17 00:00:00 2001 From: cwmoriarty <1413232+cwmoriarty@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:44:44 -0400 Subject: [PATCH] OPNsense --- vm/opnsense-vm.sh | 639 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 vm/opnsense-vm.sh diff --git a/vm/opnsense-vm.sh b/vm/opnsense-vm.sh new file mode 100644 index 0000000000..1a9d84db64 --- /dev/null +++ b/vm/opnsense-vm.sh @@ -0,0 +1,639 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: michelroegl-brunner +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE + +COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/DevScripts/main}" +source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/pve/vm-core.func") +load_functions + +#API VARIABLES +RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" +METHOD="" +APP="OPNsense" +APP_TYPE="vm" +NSAPP="opnsense-vm" +var_os="opnsense" +var_version="26.7" +FREEBSD_MAJOR="15" +# +GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') +GEN_MAC_LAN=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') + +HA=$(echo "\033[1;34m") +THIN="discard=on,ssd=1," + +header_info +echo -e "Loading..." +set -Eeo pipefail +shopt -s inherit_errexit +trap 'error_handler $LINENO "$BASH_COMMAND"' ERR +trap cleanup EXIT +trap 'post_update_to_api "failed" "130"' SIGINT +trap 'post_update_to_api "failed" "143"' SIGTERM +trap 'post_update_to_api "failed" "129"; exit 129' SIGHUP + +vm_require_arch amd64 + +function check_disk_space() { + local path="$1" + local required_gb="$2" + local available_kb=$(df -k "$path" | awk 'NR==2 {print $4}') + local available_gb=$((available_kb / 1024 / 1024)) + if [ $available_gb -lt $required_gb ]; then + return 1 + fi + return 0 +} + +TEMP_DIR=$(mktemp -d) +if ! check_disk_space "$TEMP_DIR" 20 && [ -d "/var/tmp" ] && check_disk_space "/var/tmp" 20; then + rm -rf "$TEMP_DIR" + TEMP_DIR=$(mktemp -d /var/tmp/opnsense-vm.XXXXXX) +fi +pushd "$TEMP_DIR" >/dev/null + +vm_preflight +vm_require_tools xz + +function send_line_to_vm() { + echo -e "${DGN}Sending line: ${YW}$1${CL}" + for ((i = 0; i < ${#1}; i++)); do + character=${1:i:1} + case $character in + " ") character="spc" ;; + "-") character="minus" ;; + "=") character="equal" ;; + ",") character="comma" ;; + ".") character="dot" ;; + "/") character="slash" ;; + "'") character="apostrophe" ;; + ";") character="semicolon" ;; + '\') character="backslash" ;; + '`') character="grave_accent" ;; + "[") character="bracket_left" ;; + "]") character="bracket_right" ;; + "_") character="shift-minus" ;; + "+") character="shift-equal" ;; + "?") character="shift-slash" ;; + "<") character="shift-comma" ;; + ">") character="shift-dot" ;; + '"') character="shift-apostrophe" ;; + ":") character="shift-semicolon" ;; + "|") character="shift-backslash" ;; + "~") character="shift-grave_accent" ;; + "{") character="shift-bracket_left" ;; + "}") character="shift-bracket_right" ;; + "A") character="shift-a" ;; + "B") character="shift-b" ;; + "C") character="shift-c" ;; + "D") character="shift-d" ;; + "E") character="shift-e" ;; + "F") character="shift-f" ;; + "G") character="shift-g" ;; + "H") character="shift-h" ;; + "I") character="shift-i" ;; + "J") character="shift-j" ;; + "K") character="shift-k" ;; + "L") character="shift-l" ;; + "M") character="shift-m" ;; + "N") character="shift-n" ;; + "O") character="shift-o" ;; + "P") character="shift-p" ;; + "Q") character="shift-q" ;; + "R") character="shift-r" ;; + "S") character="shift-s" ;; + "T") character="shift-t" ;; + "U") character="shift-u" ;; + "V") character="shift-v" ;; + "W") character="shift-w" ;; + "X") character="shift-x" ;; + "Y") character="shift-y" ;; + "Z") character="shift-z" ;; + "!") character="shift-1" ;; + "@") character="shift-2" ;; + "#") character="shift-3" ;; + '$') character="shift-4" ;; + "%") character="shift-5" ;; + "^") character="shift-6" ;; + "&") character="shift-7" ;; + "*") character="shift-8" ;; + "(") character="shift-9" ;; + ")") character="shift-0" ;; + esac + qm sendkey $VMID "$character" + done + qm sendkey $VMID ret +} + +function get_available_bridges() { + ip -o link show type bridge 2>/dev/null | awk -F': ' '{print $2}' | sort +} + +function guide_create_bridge() { + local brg_name="${1:-vmbr1}" + local guide_text="Only one network bridge was detected.\n\nOPNsense typically operates as a router/firewall with two interfaces (LAN and WAN), each attached to a separate Linux bridge (e.g., vmbr0 and vmbr1).\n\nTo create a second bridge:\n 1. Go to Proxmox VE Web UI -> Your Node -> System -> Network\n 2. Click 'Create' -> 'Linux Bridge'\n 3. Name it '${brg_name}' (assign a physical NIC or leave unassigned for internal routing)\n 4. Click 'Apply Configuration'\n\nAlternatively, you can create and bring up '${brg_name}' now automatically." + + if vm_dialog yesno "WAN BRIDGE SETUP" "$guide_text\n\nWould you like this script to create '${brg_name}' now?" 20 72; then + if grep -q "iface ${brg_name} inet" /etc/network/interfaces 2>/dev/null; then + echo -e "${YW}${brg_name} already exists in /etc/network/interfaces, bringing up...${CL}" + else + echo -e "${DGN}Adding ${brg_name} to /etc/network/interfaces...${CL}" + cat <>/etc/network/interfaces + +auto ${brg_name} +iface ${brg_name} inet manual + bridge-ports none + bridge-stp off + bridge-fd 0 +EOF + fi + if command -v ifreload >/dev/null 2>&1; then + ifreload -a &>/dev/null || ifup ${brg_name} &>/dev/null || true + else + ifup ${brg_name} &>/dev/null || true + fi + if ip link show "${brg_name}" &>/dev/null; then + msg_ok "Created and activated bridge ${brg_name}" + return 0 + else + vm_dialog msgbox "BRIDGE CREATION FAILED" "Could not automatically activate ${brg_name}.\nPlease configure a second bridge in Proxmox Network configuration." 10 65 + return 1 + fi + fi + return 1 +} + +function validate_ip_octets() { + local octet='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])' + [[ "$1" =~ ^${octet}\.${octet}\.${octet}\.${octet}$ ]] +} + +function prompt_router_input() { + local var_name="$1" title="$2" prompt="$3" default_value="$4" value + if vm_dialog inputbox "$title" "$prompt" 8 58 "$default_value" --cancel-button Exit-Script; then + value="$VM_DIALOG_RESULT" + printf -v "$var_name" '%s' "$value" + else + exit_script + fi +} + +function prompt_optional_static_ip() { + local ip_var="$1" gw_var="$2" mask_var="$3" prefix="$4" ip_value gw_value mask_value + prompt_router_input "$ip_var" "${prefix} IP ADDRESS" "Set a ${prefix} IP" "${!ip_var:-}" + ip_value="${!ip_var}" + if [ -z "$ip_value" ]; then + echo -e "${DGN}Using DHCP AS ${prefix} IP ADDRESS${CL}" + return 0 + fi + if ! validate_ip_octets "$ip_value"; then + msg_error "Invalid IP Address format for ${prefix} IP. Needs to be 0.0.0.0, was $ip_value" + exit 1 + fi + echo -e "${DGN}Using ${prefix} IP ADDRESS: ${BGN}$ip_value${CL}" + + prompt_router_input "$gw_var" "${prefix} GATEWAY IP ADDRESS" "Set a ${prefix} GATEWAY IP" "${!gw_var:-}" + gw_value="${!gw_var}" + if [ -z "$gw_value" ]; then + msg_error "${prefix} gateway is required for a static IP." + exit 1 + fi + if ! validate_ip_octets "$gw_value"; then + msg_error "Invalid IP Address format for ${prefix} Gateway. Needs to be 0.0.0.0, was $gw_value" + exit 1 + fi + echo -e "${DGN}Using ${prefix} GATEWAY ADDRESS: ${BGN}$gw_value${CL}" + + prompt_router_input "$mask_var" "${prefix} NETMASK" "Set a ${prefix} netmask (24 for example)" "${!mask_var:-}" + mask_value="${!mask_var}" + if [ -z "$mask_value" ]; then + msg_error "${prefix} netmask is required for a static IP." + exit 1 + fi + if [[ ! "$mask_value" =~ ^[0-9]+$ || "$mask_value" -lt 1 || "$mask_value" -gt 32 ]]; then + msg_error "Invalid ${prefix} NETMASK format. Needs to be 1-32, was $mask_value" + exit 1 + fi + echo -e "${DGN}Using ${prefix} NETMASK: ${BGN}$mask_value${CL}" +} + +function prompt_router_mac() { + local var_name="$1" title="$2" prompt="$3" default_value="$4" label="$5" value + prompt_router_input "$var_name" "$title" "$prompt" "$default_value" + value="${!var_name}" + [[ -n "$value" ]] || value="$default_value" + printf -v "$var_name" '%s' "$value" + if ! validate_mac_address "$value"; then + msg_error "Invalid ${label}: $value" + exit 1 + fi + echo -e "${DGN}Using ${label}: ${BGN}$value${CL}" +} + +function select_network_mode() { + local available_bridges bridge_count default_wan_brg + available_bridges=$(get_available_bridges) + bridge_count=$(echo "$available_bridges" | wc -l) + default_wan_brg=$(echo "$available_bridges" | grep -v "^${BRG}$" | head -n1 || true) + + if [[ "${VM_UNATTENDED:-0}" == "1" ]]; then + WAN_BRG="${VM_WAN_BRIDGE:-}" + return 0 + fi + + if [ "$bridge_count" -lt 2 ]; then + echo -e "${YW}No additional bridge available for WAN. Only '${BRG}' exists.${CL}" + echo -e "${YW}A second bridge (e.g. vmbr1) is needed for a dual-interface firewall setup.${CL}" + if guide_create_bridge "vmbr1"; then + available_bridges=$(get_available_bridges) + bridge_count=$(echo "$available_bridges" | wc -l) + default_wan_brg=$(echo "$available_bridges" | grep -v "^${BRG}$" | head -n1 || true) + fi + fi + + if [ "$bridge_count" -ge 2 ]; then + if vm_dialog radiolist "NETWORK CONFIGURATION" --cancel-button Exit-Script \ + "Choose network setup mode for OPNsense:\n" 14 70 2 \ + "dual" "Dual Interface (Firewall/Router) - uses ${default_wan_brg}" ON \ + "single" "Single Interface (Proxy/VPN/IDS Server)" OFF; then + if [ "$VM_DIALOG_RESULT" = "dual" ]; then + WAN_BRG="$default_wan_brg" + echo -e "${DGN}Network Mode: ${BGN}Dual Interface (Firewall)${CL}" + echo -e "${DGN}Using WAN Bridge: ${BGN}${WAN_BRG}${CL}" + echo -e "${DGN}Using WAN MAC Address: ${BGN}${WAN_MAC}${CL}" + else + echo -e "${DGN}Network Mode: ${BGN}Single Interface (Proxy/VPN/IDS)${CL}" + WAN_BRG="" + fi + else + exit_script + fi + else + echo -e "${DGN}Network Mode: ${BGN}Single Interface (Proxy/VPN/IDS)${CL}" + echo -e "${YW} (Only one bridge detected, dual interface requires a second bridge)${CL}" + WAN_BRG="" + fi +} + +function prompt_wan_bridge() { + local wan_bridges wan_menu=() first=true brg + wan_bridges=$(get_available_bridges | grep -v "^${BRG}$" || true) + if [ -z "$wan_bridges" ]; then + echo -e "${YW}No additional bridge available for WAN. Only '${BRG}' exists.${CL}" + echo -e "${YW}A second bridge (e.g. vmbr1) is needed for a dual-interface firewall setup.${CL}" + if guide_create_bridge "vmbr1"; then + wan_bridges=$(get_available_bridges | grep -v "^${BRG}$" || true) + fi + fi + if [ -z "$wan_bridges" ]; then + WAN_BRG="" + msg_warn "Only one bridge is available; using single-interface mode." + return 0 + fi + while IFS= read -r brg; do + if $first; then + wan_menu+=("$brg" "" "ON") + first=false + else + wan_menu+=("$brg" "" "OFF") + fi + done <<<"$wan_bridges" + + if vm_dialog radiolist "WAN BRIDGE" "Select WAN Bridge" 14 58 6 "${wan_menu[@]}"; then + WAN_BRG="$VM_DIALOG_RESULT" + [[ -n "$WAN_BRG" ]] || WAN_BRG=$(echo "$wan_bridges" | head -n1) + echo -e "${DGN}Using WAN Bridge: ${BGN}$WAN_BRG${CL}" + else + exit_script + fi +} + +function default_settings() { + vm_apply_machine_type "i440fx" + VMID=$(get_valid_nextid) + DISK_SIZE="20G" + FORMAT=",efitype=4m" + MACHINE="" + DISK_CACHE="" + HN="opnsense" + CPU_TYPE="" + CORE_COUNT="4" + RAM_SIZE="8192" + BRG="vmbr0" + IP_ADDR="" + WAN_IP_ADDR="" + LAN_GW="" + WAN_GW="" + NETMASK="" + WAN_NETMASK="" + VLAN="" + MAC=$GEN_MAC + WAN_MAC=$GEN_MAC_LAN + WAN_BRG="" + MTU="" + START_VM="yes" + METHOD="default" + + echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}" + echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}" + echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}" + echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}" + if ! ip link show "${BRG}" &>/dev/null; then + msg_error "Bridge '${BRG}' does not exist" + exit + else + echo -e "${DGN}Using LAN Bridge: ${BGN}${BRG}${CL}" + fi + echo -e "${DGN}Using LAN VLAN: ${BGN}Default${CL}" + echo -e "${DGN}Using LAN MAC Address: ${BGN}${MAC}${CL}" + select_network_mode + echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}" + echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}" + echo -e "${BL}Creating a OPNsense VM using the above default settings${CL}" +} + +function advanced_settings() { + METHOD="advanced" + IP_ADDR="" + WAN_IP_ADDR="" + LAN_GW="" + WAN_GW="" + NETMASK="" + WAN_NETMASK="" + VLAN="" + MTU="" + vm_prompt_disk_size "20G" + vm_prompt_verbose "no" + vm_prompt_start_vm "yes" + vm_prompt_vmid "${VMID:-$(get_valid_nextid)}" + vm_prompt_machine_type "i440fx" + vm_prompt_cpu_model "kvm64" + vm_prompt_disk_cache "none" + vm_prompt_hostname "opnsense" + vm_prompt_cpu_cores "4" + vm_prompt_ram "8192" + + prompt_router_input "BRG" "LAN BRIDGE" "Set a LAN Bridge" "vmbr0" + [[ -n "$BRG" ]] || BRG="vmbr0" + if ! ip link show "${BRG}" &>/dev/null; then + msg_error "Bridge '${BRG}' does not exist" + exit 1 + fi + echo -e "${DGN}Using LAN Bridge: ${BGN}$BRG${CL}" + + prompt_optional_static_ip "IP_ADDR" "LAN_GW" "NETMASK" "LAN" + prompt_wan_bridge + if [[ -n "$WAN_BRG" ]]; then + prompt_optional_static_ip "WAN_IP_ADDR" "WAN_GW" "WAN_NETMASK" "WAN" + fi + prompt_router_mac "MAC" "LAN MAC ADDRESS" "Set a LAN MAC Address" "$GEN_MAC" "LAN MAC address" + if [[ -n "$WAN_BRG" ]]; then + prompt_router_mac "WAN_MAC" "WAN MAC ADDRESS" "Set a WAN MAC Address" "$GEN_MAC_LAN" "WAN MAC address" + else + WAN_MAC="$GEN_MAC_LAN" + fi + + if vm_confirm_advanced_settings "Ready to create OPNsense VM?"; then + echo -e "${RD}Creating a OPNsense VM using the above advanced settings${CL}" + else + header_info + echo -e "${RD}Using Advanced Settings${CL}" + advanced_settings + fi +} + +vm_start_script "Use Default Settings?\n\nDefaults:\n• 4 CPU Cores\n• 8 GB RAM\n• 20 GB Disk" 13 58 +post_to_api_vm + +vm_select_storage "$HN" +msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}." +msg_info "Retrieving the URL for the OPNsense Qcow2 Disk Image" +vm_latest_from_index "https://download.freebsd.org/releases/VM-IMAGES/" "${FREEBSD_MAJOR}\.[0-9]+-RELEASE" \ + --probe "https://download.freebsd.org/releases/VM-IMAGES/{}/amd64/Latest/FreeBSD-{}-amd64.qcow2.xz" \ + --probe "https://download.freebsd.org/releases/VM-IMAGES/{}/amd64/Latest/FreeBSD-{}-amd64-ufs.qcow2.xz" \ + --probe "https://download.freebsd.org/releases/VM-IMAGES/{}/amd64/Latest/FreeBSD-{}-amd64-zfs.qcow2.xz" || exit 115 +FREEBSD_VER="$VM_INDEX_LATEST" +URL="$VM_INDEX_URL" +msg_ok "Download URL: ${CL}${BL}${URL}${CL}" + +# Check available disk space (require at least 20GB for safety) +if ! check_disk_space "$TEMP_DIR" 20; then + AVAILABLE_GB=$(df -h "$TEMP_DIR" | awk 'NR==2 {print $4}') + msg_error "Insufficient disk space in temporary directory ($TEMP_DIR)." + msg_error "Available: ${AVAILABLE_GB}, Required: ~20GB for FreeBSD image decompression." + msg_error "Please free up space or ensure /tmp has sufficient storage." + exit 214 +fi + +msg_info "Downloading FreeBSD Image" +# A mirror serving an error page returns 200, so size decides whether this +# is an image. Anything real here is far above 5 MB. +CACHE_FILE="$(vm_image_cache_path "$URL")" +vm_fetch_image "$URL" "$CACHE_FILE" --cache --verify-xz --min-bytes $((5 * 1024 * 1024)) || exit 1 +echo -en "\e[1A\e[0K" +msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}" + +# Check disk space again before decompression +if ! check_disk_space "$TEMP_DIR" 15; then + AVAILABLE_GB=$(df -h "$TEMP_DIR" | awk 'NR==2 {print $4}') + msg_error "Insufficient disk space for decompression." + msg_error "Available: ${AVAILABLE_GB}, Required: ~15GB for decompressed image." + exit 214 +fi + +FILE="$TEMP_DIR/FreeBSD.qcow2" +vm_extract_image "$CACHE_FILE" "$FILE" || exit 115 +FILE="$VM_IMAGE_FILE" + +vm_define_disk_references 1 + +msg_info "Creating a OPNsense VM" +qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE -balloon 0 \ + -name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC,firewall=0,queues=$CORE_COUNT$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci +vm_mark_created + +# Retry pvesm alloc on transient zfs_request "got timeout" errors (#14127) +alloc_attempt=1 +alloc_max=4 +alloc_delay=5 +while :; do + alloc_err=$(pvesm alloc $STORAGE $VMID $DISK0 4M 2>&1 >/dev/null) && break + if [[ "$alloc_err" == *"got timeout"* && $alloc_attempt -lt $alloc_max ]]; then + echo -e "${YW}[WARN]${CL} pvesm alloc hit zfs timeout (attempt $alloc_attempt/$alloc_max), retrying in ${alloc_delay}s..." + pvesm free "${DISK0_REF}" &>/dev/null || true + sleep "$alloc_delay" + alloc_attempt=$((alloc_attempt + 1)) + alloc_delay=$((alloc_delay * 2)) + continue + fi + echo -e "$alloc_err" >&2 + exit 220 +done +vm_import_disk "$VMID" "$FILE" "$STORAGE" +qm set $VMID \ + -efidisk0 ${DISK0_REF}${FORMAT} \ + -scsi0 "${VM_IMPORTED_DISK}",${DISK_CACHE}${THIN}size=2G \ + -boot order=scsi0 \ + -serial0 socket \ + -tags community-script >/dev/null +vm_resize_disk +set_description + +msg_info "Bridge interfaces are being added." +qm set $VMID \ + -net0 virtio,bridge=${BRG},macaddr=${MAC},firewall=0,queues=${CORE_COUNT}${VLAN}${MTU} 2>/dev/null +if [ -n "$WAN_BRG" ]; then + qm set $VMID \ + -net1 virtio,bridge=${WAN_BRG},macaddr=${WAN_MAC},firewall=0,queues=${CORE_COUNT} 2>/dev/null +fi +msg_ok "Bridge interfaces have been successfully added." + +msg_ok "Created a OPNsense VM ${CL}${BL}(${HN})" +msg_ok "Starting OPNsense VM (Patience this takes 20-30 minutes)" +$STD qm start $VMID +sleep 90 +send_line_to_vm "root" +sleep 2 +send_line_to_vm "" +send_line_to_vm "for i in \$(seq 1 60); do fetch -q https://raw.githubusercontent.com/opnsense/update/master/src/bootstrap/opnsense-bootstrap.sh.in && break; sleep 5; done" +sleep 5 +# FreeBSD 15+ VM images ship the base system as pkgbase packages; the bootstrap's +# "delete all packages" step would remove the running base system (/bin/rm etc.) +# and brick the VM. Deregister them from the pkg db first - the files stay in +# place and OPNsense replaces base and kernel with its own sets afterwards. +send_line_to_vm "echo \"PRAGMA foreign_keys=ON; DELETE FROM packages WHERE name LIKE 'FreeBSD-%';\" | pkg shell" +sleep 5 +send_line_to_vm "sh ./opnsense-bootstrap.sh.in -y -f -r ${var_version}" +msg_ok "OPNsense VM is being installed, do not close the terminal, or the installation will fail." +# The bootstrap ends with an automatic reboot into OPNsense. While it runs the +# console keeps changing (download progress, package installs); once the VM has +# settled at the login prompt the screen stays static. Poll a screendump hash +# and continue after 3 minutes without change, bounded by a floor (the build +# never finishes faster) and a ceiling for slow machines. If no screendump can +# be captured at all, fall back to a fixed wait. +SCREEN_PPM="${TEMP_DIR}/screen-${VMID}.ppm" + +function screen_hash() { + # Remove the previous dump first: a stale file from an earlier successful + # dump must not simulate a static screen when later dumps start failing. + # Note: "qm monitor" is unusable here - its readline attaches to /dev/tty + # even with piped stdin and captures the terminal, so use the API instead. + rm -f "$SCREEN_PPM" + timeout 10 pvesh create /nodes/$(hostname -s)/qemu/$VMID/monitor --command "screendump ${SCREEN_PPM}" >/dev/null 2>&1 || true + md5sum "$SCREEN_PPM" 2>/dev/null | cut -d' ' -f1 || true +} + +build_elapsed=300 +build_stable=0 +screen_ok=0 +hash_a="" +hash_b="" +sleep 300 +while [ $build_stable -lt 6 ] && [ $build_elapsed -lt 2400 ]; do + sleep 30 + build_elapsed=$((build_elapsed + 30)) + new_hash=$(screen_hash) + if [ -n "$new_hash" ]; then + screen_ok=1 + # The login prompt cursor may blink: a screen alternating between the same + # two frames (A/B/A/B) counts as stable, anything new resets the counter + if [ "$new_hash" = "$hash_a" ] || [ "$new_hash" = "$hash_b" ]; then + build_stable=$((build_stable + 1)) + else + build_stable=0 + fi + else + build_stable=0 + fi + hash_b="$hash_a" + hash_a="$new_hash" + if [ -n "$new_hash" ]; then + echo -e "${DGN}Waiting for OPNsense build: ${YW}$((build_elapsed / 60))min elapsed, screen ${new_hash:0:8}, stable ${build_stable}/6${CL}" + else + echo -e "${DGN}Waiting for OPNsense build: ${YW}$((build_elapsed / 60))min elapsed, screendump failed${CL}" + fi + # No working screendump after several attempts: fixed wait instead + if [ $screen_ok -eq 0 ] && [ $build_elapsed -ge 480 ]; then + msg_warn "Console screendump not available on this system - falling back to a fixed wait (12 minutes)." + sleep 720 + build_elapsed=$((build_elapsed + 720)) + break + fi +done +msg_warn "Console wait ended after $((build_elapsed / 60)) minutes; a static screen does not prove installation succeeded. Verify the OPNsense console." +send_line_to_vm "root" +send_line_to_vm "opnsense" +send_line_to_vm "2" + +if [ "$IP_ADDR" != "" ]; then + send_line_to_vm "1" + send_line_to_vm "n" + send_line_to_vm "${IP_ADDR}" + send_line_to_vm "${NETMASK}" + send_line_to_vm "${LAN_GW}" + send_line_to_vm "n" + send_line_to_vm " " + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm " " + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm "n" +else + send_line_to_vm "1" + send_line_to_vm "y" + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm " " + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm "n" +fi +#Wait for config changes to be saved +sleep 20 +if [ -n "$WAN_BRG" ] && [ "$WAN_IP_ADDR" != "" ]; then + send_line_to_vm "2" + send_line_to_vm "2" + send_line_to_vm "n" + send_line_to_vm "${WAN_IP_ADDR}" + send_line_to_vm "${WAN_NETMASK}" + send_line_to_vm "${WAN_GW}" + send_line_to_vm "n" + send_line_to_vm " " + send_line_to_vm "n" + send_line_to_vm " " + send_line_to_vm "n" + send_line_to_vm "n" + send_line_to_vm "n" +fi +sleep 10 +send_line_to_vm "0" +if [[ "$START_VM" == "no" ]]; then + msg_info "Shutting down OPNsense as requested" + $STD qm shutdown "$VMID" --timeout 120 + msg_ok "OPNsense VM shut down" +else + msg_ok "OPNsense VM is running" +fi + +if [ "$IP_ADDR" != "" ]; then + LAN_URL="http://${IP_ADDR}" +else + LAN_URL="DHCP - check the OPNsense console or your leases" +fi + +vm_print_summary \ + "LAN URL=${LAN_URL}" \ + "LAN Bridge=${BRG}" \ + "WAN Bridge=${WAN_BRG:-single-interface mode}" \ + "OPNsense Version=${var_version}" \ + "FreeBSD Base=${FREEBSD_VER}" +vm_next_steps \ + "Verify the guest bootstrap and network configuration in the OPNsense console." \ + "Login as root with password opnsense after successful bootstrap, then change the password immediately." \ + "Keep WAN and LAN isolated appropriately; single-interface mode is intended for proxy, VPN, or IDS use cases." +vm_finish "VM creation completed; verify the guest bootstrap and network configuration in the console."