From b087c0bbc3fef823caac68156ca29c9cc58d861a Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:09:45 +0200 Subject: [PATCH] Refactor: OPNsense VM (#17785) --- vm/opnsense-vm.sh | 860 ++++++++++++++-------------------------------- 1 file changed, 256 insertions(+), 604 deletions(-) diff --git a/vm/opnsense-vm.sh b/vm/opnsense-vm.sh index 785e20c9e..bdb3cc508 100644 --- a/vm/opnsense-vm.sh +++ b/vm/opnsense-vm.sh @@ -1,27 +1,17 @@ -#!/usr/bin/env bash - +#!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG -# Author: michelroegl-brunner +# Author: michelroegl-brunner | MickLesk (CanbiZ) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) +COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}" +source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/pve/vm-core.func") +load_functions -function header_info { - clear - cat <<"EOF" - ____ ____ _ __ - / __ \/ __ \/ | / /_______ ____ ________ - / / / / /_/ / |/ / ___/ _ \/ __ \/ ___/ _ \ -/ /_/ / ____/ /| (__ ) __/ / / (__ ) __/ -\____/_/ /_/ |_/____/\___/_/ /_/____/\___/ - -EOF -} -header_info -echo -e "Loading..." #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" @@ -30,70 +20,20 @@ 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/.$//') -YW=$(echo "\033[33m") -BL=$(echo "\033[36m") HA=$(echo "\033[1;34m") -RD=$(echo "\033[01;31m") -BGN=$(echo "\033[4;92m") -GN=$(echo "\033[1;92m") -DGN=$(echo "\033[32m") -CL=$(echo "\033[m") -BFR="\\r\\033[K" -HOLD="-" -CM="${GN}✓${CL}" -CROSS="${RD}✗${CL}" +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 -function error_handler() { - local exit_code="$?" - local line_number="$1" - local command="$2" - post_update_to_api "failed" "$exit_code" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" - cleanup_vmid -} -function get_valid_nextid() { - local try_id - try_id=$(pvesh get /cluster/nextid) - while true; do - if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then - try_id=$((try_id + 1)) - continue - fi - if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then - try_id=$((try_id + 1)) - continue - fi - break - done - echo "$try_id" -} - -function cleanup_vmid() { - if qm status $VMID &>/dev/null; then - qm stop $VMID &>/dev/null - qm destroy $VMID &>/dev/null - fi -} - -function cleanup() { - local exit_code=$? - popd >/dev/null - if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then - if [[ $exit_code -eq 0 ]]; then - post_update_to_api "done" "none" - else - post_update_to_api "failed" "$exit_code" - fi - fi - rm -rf $TEMP_DIR -} +vm_require_arch amd64 function check_disk_space() { local path="$1" @@ -106,16 +46,16 @@ function check_disk_space() { return 0 } -# Use disk-backed temp directory to avoid tmpfs/RAM size limits in /tmp -if [ -d "/var/tmp" ] && check_disk_space "/var/tmp" 20; then - TEMP_DIR=$(mktemp -d /var/tmp/opnsense-vm.XXXXXX) -elif [ -d "/tmp" ] && check_disk_space "/tmp" 20; then - TEMP_DIR=$(mktemp -d) -else - # Fallback: try /var/tmp anyway, disk space check will catch it later +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 +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 @@ -186,95 +126,139 @@ function send_line_to_vm() { qm sendkey $VMID ret } -if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "OPNsense VM" --yesno "This will create a New OPNsense VM. Proceed?" 10 58); then - : -else - header_info && echo -e "⚠ User exited script \n" && exit -fi - -function msg_info() { - local msg="$1" - echo -ne " ${HOLD} ${YW}${msg}..." -} - -function msg_ok() { - local msg="$1" - echo -e "${BFR} ${CM} ${GN}${msg}${CL}" -} - -function msg_error() { - local msg="$1" - echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" -} - -# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported. -# Supported: Proxmox VE 8.0.x – 8.9.x, 9.0 and 9.2 -pve_check() { - local PVE_VER - PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - - # Check for Proxmox VE 8.x: allow 8.0–8.9 - if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR < 0 || MINOR > 9)); then - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported: Proxmox VE version 8.0 – 8.9" - exit 105 - fi - return 0 - fi - - # Check for Proxmox VE 9.x: allow 9.0 and 9.2 - if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR < 0 || MINOR > 2)); then - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported: Proxmox VE version 9.0 – 9.2" - exit 105 - fi - return 0 - fi - - # All other unsupported versions - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.2" - exit 105 -} - -function arch_check() { - if [ "$(dpkg --print-architecture)" != "amd64" ]; then - echo -e "\n ${CROSS} This script will not work with PiMox! \n" - echo -e "Exiting..." - sleep 2 - exit - fi -} - -function ssh_check() { - if command -v pveversion >/dev/null 2>&1; then - if [ -n "${SSH_CLIENT:+x}" ]; then - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then - echo "you've been warned" - else - clear - exit - fi - fi - fi -} - -function exit-script() { - clear - echo -e "⚠ User exited script \n" - exit -} - function get_available_bridges() { ip -o link show type bridge 2>/dev/null | awk -F': ' '{print $2}' | sort } +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:-}" + elif [ "$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 + 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="" @@ -297,12 +281,6 @@ function default_settings() { START_VM="yes" METHOD="default" - # Detect available bridges - local AVAILABLE_BRIDGES - AVAILABLE_BRIDGES=$(get_available_bridges) - local BRIDGE_COUNT - BRIDGE_COUNT=$(echo "$AVAILABLE_BRIDGES" | wc -l) - 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}" @@ -315,289 +293,55 @@ function default_settings() { fi echo -e "${DGN}Using LAN VLAN: ${BGN}Default${CL}" echo -e "${DGN}Using LAN MAC Address: ${BGN}${MAC}${CL}" - - # Determine available network modes based on bridge count - local DEFAULT_WAN_BRG - DEFAULT_WAN_BRG=$(echo "$AVAILABLE_BRIDGES" | grep -v "^${BRG}$" | head -n1 || true) - - if [ "$BRIDGE_COUNT" -ge 2 ]; then - # Multiple bridges available - offer dual or single mode - if NETWORK_MODE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "NETWORK CONFIGURATION" --radiolist --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 \ - 3>&1 1>&2 2>&3); then - if [ "$NETWORK_MODE" = "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 - # Only one bridge available - single interface mode only - 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 + 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() { - local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' METHOD="advanced" - [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid) - while true; do - if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z "$VMID" ]; then - VMID=$(get_valid_nextid) - fi - if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then - echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" - sleep 2 - continue - fi - echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" - break - else - exit-script - fi - done + IP_ADDR="" + WAN_IP_ADDR="" + LAN_GW="" + WAN_GW="" + NETMASK="" + WAN_NETMASK="" + VLAN="" + MTU="" + vm_prompt_disk_size "20G" + vm_prompt_keyboard + 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" - if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \ - "i440fx" "Machine i440fx" ON \ - "q35" "Machine q35" OFF \ - 3>&1 1>&2 2>&3); then - if [ $MACH = q35 ]; then - echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" - FORMAT="" - MACHINE=" -machine q35" - else - echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" - FORMAT=",efitype=4m" - MACHINE="" - fi + 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 - exit-script + WAN_MAC="$GEN_MAC_LAN" fi - if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \ - "0" "KVM64 (Default)" ON \ - "1" "Host" OFF \ - 3>&1 1>&2 2>&3); then - if [ $CPU_TYPE1 = "1" ]; then - echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}" - CPU_TYPE=" -cpu host" - else - echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}" - CPU_TYPE="" - fi - else - exit-script - fi - - if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \ - "0" "None (Default)" ON \ - "1" "Write Through" OFF \ - 3>&1 1>&2 2>&3); then - if [ $DISK_CACHE = "1" ]; then - echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}" - DISK_CACHE="cache=writethrough," - else - echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}" - DISK_CACHE="" - fi - else - exit-script - fi - - if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 OPNsense --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z "$VM_NAME" ]; then - HN="OPNsense" - else - HN=$(echo "${VM_NAME,,}" | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//') - if [ "$HN" != "${VM_NAME,,}" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --title "HOSTNAME ADJUSTED" --msgbox "Invalid characters detected. Hostname has been adjusted to:\n\n $HN" 10 58 - fi - fi - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" - else - exit-script - fi - - while true; do - if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 4 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z "$CORE_COUNT" ]; then CORE_COUNT="4"; fi - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - break - fi - whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "CPU Cores must be a positive integer (e.g., 4)." 8 58 - else - exit-script - fi - done - - while true; do - if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 8192 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z "$RAM_SIZE" ]; then RAM_SIZE="8192"; fi - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - break - fi - whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "RAM Size must be a positive integer in MiB (e.g., 8192)." 8 58 - else - exit-script - fi - done - - if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $BRG ]; then - BRG="vmbr0" - fi - if ! ip link show "${BRG}" &>/dev/null; then - msg_error "Bridge '${BRG}' does not exist" - exit - fi - echo -e "${DGN}Using LAN Bridge: ${BGN}$BRG${CL}" - else - exit-script - fi - - if IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN IP" 8 58 $IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $IP_ADDR ]; then - echo -e "${DGN}Using DHCP AS LAN IP ADDRESS${CL}" - else - if [[ -n "$IP_ADDR" && ! "$IP_ADDR" =~ $ip_regex ]]; then - msg_error "Invalid IP Address format for LAN IP. Needs to be 0.0.0.0, was $IP_ADDR" - exit - fi - echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$IP_ADDR${CL}" - if LAN_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN GATEWAY IP" 8 58 $LAN_GW --title "LAN GATEWAY IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_GW ]; then - echo -e "${DGN}Gateway needs to be set if ip is not dhcp${CL}" - exit-script - fi - if [[ -n "$LAN_GW" && ! "$LAN_GW" =~ $ip_regex ]]; then - msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was $LAN_GW" - exit - fi - echo -e "${DGN}Using LAN GATEWAY ADDRESS: ${BGN}$LAN_GW${CL}" - fi - if NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN netmask (24 for example)" 8 58 $NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $NETMASK ]; then - echo -e "${DGN}Netmask needs to be set if ip is not dhcp${CL}" - fi - if [[ -n "$NETMASK" && ! ("$NETMASK" =~ ^[0-9]+$ && "$NETMASK" -ge 1 && "$NETMASK" -le 32) ]]; then - msg_error "Invalid LAN NETMASK format. Needs to be 1-32, was $NETMASK" - exit - fi - echo -e "${DGN}Using LAN NETMASK: ${BGN}$NETMASK${CL}" - else - exit-script - fi - fi - else - exit-script - fi - - # Build WAN bridge selection from available bridges (excluding LAN bridge) - local WAN_BRIDGES - WAN_BRIDGES=$(get_available_bridges | grep -v "^${BRG}$" || true) - if [ -z "$WAN_BRIDGES" ]; then - msg_error "No additional bridge available for WAN. Only '${BRG}' exists." - msg_error "Create a second bridge (e.g. vmbr1) in Proxmox network config first." - exit - fi - local WAN_MENU=() - local first=true - 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 WAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "WAN BRIDGE" --radiolist "Select WAN Bridge" 14 58 6 \ - "${WAN_MENU[@]}" 3>&1 1>&2 2>&3); then - if [ -z "$WAN_BRG" ]; then - WAN_BRG=$(echo "$WAN_BRIDGES" | head -n1) - fi - echo -e "${DGN}Using WAN Bridge: ${BGN}$WAN_BRG${CL}" - else - exit-script - fi - - if WAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN IP" 8 58 $WAN_IP_ADDR --title "WAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $WAN_IP_ADDR ]; then - echo -e "${DGN}Using DHCP AS WAN IP ADDRESS${CL}" - else - if [[ -n "$WAN_IP_ADDR" && ! "$WAN_IP_ADDR" =~ $ip_regex ]]; then - msg_error "Invalid IP Address format for WAN IP. Needs to be 0.0.0.0, was $WAN_IP_ADDR" - exit - fi - echo -e "${DGN}Using WAN IP ADDRESS: ${BGN}$WAN_IP_ADDR${CL}" - if WAN_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN GATEWAY IP" 8 58 $WAN_GW --title "WAN GATEWAY IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $WAN_GW ]; then - echo -e "${DGN}Gateway needs to be set if ip is not dhcp${CL}" - exit-script - fi - if [[ -n "$WAN_GW" && ! "$WAN_GW" =~ $ip_regex ]]; then - msg_error "Invalid IP Address format for WAN Gateway. Needs to be 0.0.0.0, was $WAN_GW" - exit - fi - echo -e "${DGN}Using WAN GATEWAY ADDRESS: ${BGN}$WAN_GW${CL}" - else - exit-script - fi - if WAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN netmask (24 for example)" 8 58 $WAN_NETMASK --title "WAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $WAN_NETMASK ]; then - echo -e "${DGN}WAN Netmask needs to be set if ip is not dhcp${CL}" - fi - if [[ -n "$WAN_NETMASK" && ! ("$WAN_NETMASK" =~ ^[0-9]+$ && "$WAN_NETMASK" -ge 1 && "$WAN_NETMASK" -le 32) ]]; then - msg_error "Invalid WAN NETMASK format. Needs to be 1-32, was $WAN_NETMASK" - exit - fi - echo -e "${DGN}Using WAN NETMASK: ${BGN}$WAN_NETMASK${CL}" - else - exit-script - fi - fi - else - exit-script - fi - if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MAC1 ]; then - MAC="$GEN_MAC" - else - MAC="$MAC1" - fi - echo -e "${DGN}Using LAN MAC Address: ${BGN}$MAC${CL}" - else - exit-script - fi - - if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC_LAN --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MAC2 ]; then - WAN_MAC="$GEN_MAC_LAN" - else - WAN_MAC="$MAC2" - fi - echo -e "${DGN}Using WAN MAC Address: ${BGN}$WAN_MAC${CL}" - else - exit-script - fi - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OPNsense VM?" --no-button Do-Over 10 58); then + 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 @@ -606,75 +350,18 @@ function advanced_settings() { fi } -function start_script() { - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then - header_info - echo -e "${BL}Using Default Settings${CL}" - default_settings - else - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings - fi -} - -arch_check -pve_check -ssh_check -start_script +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 -msg_info "Validating Storage" -while read -r line; do - TAG=$(echo $line | awk '{print $1}') - TYPE=$(echo $line | awk '{printf "%-10s", $2}') - FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}') - ITEM=" Type: $TYPE Free: $FREE " - OFFSET=2 - if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then - MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) - fi - STORAGE_MENU+=("$TAG" "$ITEM" "OFF") -done < <(pvesm status -content images | awk 'NR>1') -VALID=$(pvesm status -content images | awk 'NR>1') -if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." - exit -elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then - STORAGE=${STORAGE_MENU[0]} -else - while [ -z "${STORAGE:+x}" ]; do - STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ - "Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \ - 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) - done -fi -msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." +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" -# Use latest stable FreeBSD amd64 qcow2 VM image matching FREEBSD_MAJOR -RELEASE_LIST="$(curl -s https://download.freebsd.org/releases/VM-IMAGES/ | - grep -Eo "${FREEBSD_MAJOR}\.[0-9]+-RELEASE" | - sort -Vr | - uniq)" -URL="" -FREEBSD_VER="" -for ver in $RELEASE_LIST; do - # FreeBSD 15+ publishes separate -ufs/-zfs images instead of a generic one - for variant in "" "-ufs" "-zfs"; do - candidate="https://download.freebsd.org/releases/VM-IMAGES/${ver}/amd64/Latest/FreeBSD-${ver}-amd64${variant}.qcow2.xz" - if curl -fsI "$candidate" >/dev/null 2>&1; then - FREEBSD_VER="$ver" - URL="$candidate" - break 2 - fi - done -done -if [ -z "$URL" ]; then - msg_error "Could not find a FreeBSD ${FREEBSD_MAJOR}.x amd64 qcow2 image." - exit 115 -fi +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) @@ -687,9 +374,12 @@ if ! check_disk_space "$TEMP_DIR" 20; then fi msg_info "Downloading FreeBSD Image" -curl -f#SL -o "$(basename "$URL")" "$URL" +# 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 "$URL")${CL}" +msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}" # Check disk space again before decompression if ! check_disk_space "$TEMP_DIR" 15; then @@ -699,49 +389,19 @@ if ! check_disk_space "$TEMP_DIR" 15; then exit 214 fi -msg_info "Decompressing FreeBSD Image (this may take a few minutes)" -FILE=FreeBSD.qcow2 -if ! unxz -cv $(basename $URL) >${FILE}; then - msg_error "Failed to decompress FreeBSD image." - msg_error "This is usually caused by insufficient disk space." - df -h "$TEMP_DIR" - exit 115 -fi +FILE="$TEMP_DIR/FreeBSD.qcow2" +vm_extract_image "$CACHE_FILE" "$FILE" || exit 115 +FILE="$VM_IMAGE_FILE" -# Remove the compressed file to save space -rm -f "$(basename "$URL")" -msg_ok "Decompressed ${CL}${BL}${FILE}${CL}" - -STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}') -case $STORAGE_TYPE in -nfs | dir) - DISK_EXT=".qcow2" - DISK_REF="$VMID/" - DISK_IMPORT="-format qcow2" - THIN="" - ;; -btrfs) - DISK_EXT=".raw" - DISK_REF="$VMID/" - DISK_IMPORT="-format raw" - FORMAT=",efitype=4m" - THIN="" - ;; -*) - DISK_EXT="" - DISK_REF="" - DISK_IMPORT="-format raw" - ;; -esac -for i in {0,1}; do - disk="DISK$i" - eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-} - eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk} -done +vm_define_disk_references 1 +vm_claim_vmid 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 \ - -name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci +# A firewall VM filters itself: Proxmox's per-NIC firewall off, and virtio +# multiqueue matched to the cores so routing scales past one vCPU. +qm create $VMID -agent 1${MACHINE} -tablet 0 -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 @@ -760,66 +420,34 @@ while :; do echo -e "$alloc_err" >&2 exit 220 done -qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} &>/dev/null +vm_import_disk "$VMID" "$FILE" "$STORAGE" qm set $VMID \ -efidisk0 ${DISK0_REF}${FORMAT} \ - -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=2G \ + -scsi0 "${VM_IMPORTED_DISK}",${DISK_CACHE}${THIN}size=2G \ -boot order=scsi0 \ -serial0 socket \ -tags community-script >/dev/null -qm resize $VMID scsi0 20G >/dev/null -DESCRIPTION=$( - cat < - - Logo - - -

OPNsense VM

- -

- - spend Coffee - -

- - - - GitHub - - - - Discussions - - - - Issues - - -EOF -) -qm set $VMID -description "$DESCRIPTION" >/dev/null +vm_resize_disk +set_description msg_info "Bridge interfaces are being added." qm set $VMID \ - -net0 virtio,bridge=${BRG},macaddr=${MAC}${VLAN}${MTU} 2>/dev/null + -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)" -qm start $VMID +msg_ok "Starting OPNsense VM (the bootstrap takes 10-30 minutes)" +$STD qm start $VMID sleep 90 send_line_to_vm "root" sleep 2 send_line_to_vm "" -send_line_to_vm "fetch https://raw.githubusercontent.com/opnsense/update/master/src/bootstrap/opnsense-bootstrap.sh.in" -if [ -n "$WAN_BRG" ]; then - msg_info "Adding WAN interface" - qm set $VMID \ - -net1 virtio,bridge=${WAN_BRG},macaddr=${WAN_MAC} &>/dev/null - msg_ok "WAN interface added" - sleep 5 # Brief pause after adding network interface -fi +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 @@ -877,13 +505,22 @@ while [ $build_stable -lt 6 ] && [ $build_elapsed -lt 2400 ]; do fi # No working screendump after several attempts: fixed wait instead if [ $screen_ok -eq 0 ] && [ $build_elapsed -ge 480 ]; then - msg_error "Console screendump not available on this system - falling back to a fixed wait (12 minutes)." + 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_ok "OPNsense build finished after $((build_elapsed / 60)) minutes" +if [ $build_stable -ge 6 ]; then + msg_ok "OPNsense console settled after $((build_elapsed / 60)) minutes" +else + msg_warn "The console did not settle within $((build_elapsed / 60)) minutes; continuing, verify the OPNsense console afterwards" +fi +# The answers below are matched to OPNsense's console dialog (setaddr.php). +# One prompt ("via WAN tracking?") only appears when WAN has DHCP6, so a spare +# "n" is sent for it; where it is absent, OPNsense re-asks the IPv6 address and +# the following ENTER lands there. Invalid answers are re-asked, empty ones take +# the default, which keeps the sequence safe in both dialog variants. send_line_to_vm "root" send_line_to_vm "opnsense" send_line_to_vm "2" @@ -933,13 +570,28 @@ if [ -n "$WAN_BRG" ] && [ "$WAN_IP_ADDR" != "" ]; then fi sleep 10 send_line_to_vm "0" -msg_ok "Started OPNsense VM" - -msg_ok "Completed successfully!\n" -if [ "$IP_ADDR" != "" ]; then - echo -e "${INFO}${YW} Access it using the following URL:${CL}" - echo -e "${TAB}${GATEWAY}${BGN}http://${IP_ADDR}${CL}" +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 - echo -e "${INFO}${YW} LAN IP was DHCP.${CL}" - echo -e "${INFO}${BGN}To find the IP login to the VM shell${CL}" + 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."