-
-
Save jaminmc/7e786a8947746439f7b8a8e2726e629d to your computer and use it in GitHub Desktop.
| #!/bin/bash | |
| # Create an OpenWrt LXC on Proxmox (x86_64). | |
| # Stable / old-stable / RC / snapshots | |
| # Saves last-run settings to /root/.openwrt-proxmox-lxc.conf (never the password) | |
| # Default resource values (overridden by last-run config if present) | |
| DEFAULT_MEMORY="256" # MB | |
| DEFAULT_CORES="2" # CPU cores | |
| DEFAULT_STORAGE="0.5" # GB | |
| DEFAULT_LAN_CIDR="10.23.45.1/24" # LAN IPv4/prefix | |
| ARCH="x86_64" # Architecture | |
| TEMPLATE_DIR="/var/lib/vz/template/cache" # Default template location | |
| CONFIG_FILE="/root/.openwrt-proxmox-lxc.conf" | |
| OW_VERSIONS_JSON="https://downloads.openwrt.org/.versions.json" | |
| OW_FALLBACK_STABLE="25.12.5" | |
| OW_FALLBACK_OLDSTABLE="24.10.8" | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| NC='\033[0m' | |
| # Exit handler | |
| exit_script() { | |
| local code=$1 | |
| local msg=$2 | |
| [ -n "$msg" ] && echo -e "${RED}$msg${NC}" | |
| exit "$code" | |
| } | |
| # Must run as root | |
| [ "$EUID" -ne 0 ] && exit_script 1 "This script must be run as root" | |
| # Check required commands | |
| for cmd in wget pct pvesm ip curl whiptail pvesh bridge stat tar numfmt; do | |
| command -v "$cmd" &>/dev/null || exit_script 1 "Required command not found: $cmd" | |
| done | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Helper functions | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| whiptail_radiolist() { | |
| local title="$1" prompt="$2" height="$3" width="$4" items=("${@:5}") | |
| local rows=$(( ${#items[@]} / 3 )) | |
| local list_h="$rows" | |
| # Keep the item list inside the box; leave room for title + prompt + buttons | |
| [ "$list_h" -gt 12 ] && list_h=12 | |
| [ "$height" -lt $((list_h + 8)) ] && height=$((list_h + 8)) | |
| [ "$width" -lt 72 ] && width=72 | |
| local selection | |
| selection=$(whiptail --title "$title" --radiolist "$prompt" "$height" "$width" "$list_h" "${items[@]}" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| echo "$selection" | |
| } | |
| whiptail_input() { | |
| local title="$1" prompt="$2" default="$3" var="$4" | |
| local height="${5:-11}" | |
| local width="${6:-74}" | |
| local input | |
| input=$(whiptail --title "$title" --inputbox "${prompt}" "$height" "$width" "$default" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| eval "$var=\"${input:-$default}\"" | |
| } | |
| json_string_field() { | |
| printf '%s' "$1" | tr -d '\n' | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | |
| } | |
| fetch_versions_json() { | |
| curl -fsSL --max-time 20 -A "openwrt-proxmox-lxc" "$OW_VERSIONS_JSON" 2>/dev/null | |
| } | |
| scrape_latest_release_dir() { | |
| curl -fsSL --max-time 20 -A "openwrt-proxmox-lxc" "https://downloads.openwrt.org/releases/" 2>/dev/null | | |
| grep -oE 'href="[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?/"' | | |
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?' | |
| } | |
| parse_versions_list() { | |
| printf '%s' "$1" | tr -d '\n' | | |
| sed -n 's/.*"versions_list"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p' | | |
| tr -d '"' | tr ',' '\n' | tr -d ' ' | grep -E '^[0-9]+\.[0-9]+' | |
| } | |
| detect_versions() { | |
| local json scraped newest | |
| json=$(fetch_versions_json) | |
| STABLE_VER=$(json_string_field "$json" "stable_version") | |
| OLDSTABLE_VER=$(json_string_field "$json" "oldstable_version") | |
| UPCOMING_VER=$(json_string_field "$json" "upcoming_version") | |
| mapfile -t VERSIONS_LIST < <(parse_versions_list "$json") | |
| if [ -z "$STABLE_VER" ]; then | |
| scraped=$(scrape_latest_release_dir) | |
| mapfile -t VERSIONS_LIST < <(printf '%s\n' "$scraped" | sort -uV | tac) | |
| STABLE_VER=$(printf '%s\n' "${VERSIONS_LIST[@]}" | grep -vE -- '-(rc|beta|alpha|test)' | sort -V | tail -1) | |
| NEWEST_VER=$(printf '%s\n' "${VERSIONS_LIST[@]}" | sort -V | tail -1) | |
| else | |
| newest=$(printf '%s\n' "${VERSIONS_LIST[@]}" | sort -V | tail -1) | |
| NEWEST_VER="${UPCOMING_VER:-$newest}" | |
| [ -z "$NEWEST_VER" ] && NEWEST_VER="$STABLE_VER" | |
| fi | |
| [ -z "$STABLE_VER" ] && STABLE_VER="$OW_FALLBACK_STABLE" | |
| [ -z "$OLDSTABLE_VER" ] && OLDSTABLE_VER="$OW_FALLBACK_OLDSTABLE" | |
| [ -z "$NEWEST_VER" ] && NEWEST_VER="$STABLE_VER" | |
| [ ${#VERSIONS_LIST[@]} -eq 0 ] && VERSIONS_LIST=("$STABLE_VER" "$OLDSTABLE_VER") | |
| } | |
| version_gt() { | |
| [ -n "$1" ] && [ -n "$2" ] && [ "$1" != "$2" ] && | |
| [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ] | |
| } | |
| pkg_tool_for_version() { | |
| local ver="$1" | |
| if [ "$ver" = "snapshot" ]; then | |
| echo apk | |
| return | |
| fi | |
| local major="${ver%%.*}" | |
| if [ "$major" -ge 25 ] 2>/dev/null; then | |
| echo apk | |
| else | |
| echo opkg | |
| fi | |
| } | |
| series_of() { | |
| local v="$1" | |
| v="${v%-rc*}" | |
| echo "${v%.*}" | |
| } | |
| version_label() { | |
| local v="$1" | |
| if [ "$v" = "$STABLE_VER" ]; then | |
| echo "Current stable" | |
| elif [ "$v" = "$OLDSTABLE_VER" ]; then | |
| echo "Old stable" | |
| elif [ -n "$UPCOMING_VER" ] && [ "$v" = "$UPCOMING_VER" ]; then | |
| echo "Upcoming RC" | |
| elif [[ "$v" == *"-rc"* ]]; then | |
| echo "Release candidate" | |
| else | |
| echo "Release" | |
| fi | |
| } | |
| select_version() { | |
| local prefer="$1" | |
| local stable_series old_series v state | |
| local -a menu=() | |
| local count=0 max=16 | |
| stable_series=$(series_of "$STABLE_VER") | |
| old_series=$(series_of "$OLDSTABLE_VER") | |
| state="OFF" | |
| [ "$prefer" = "snapshot" ] && state="ON" | |
| menu+=("snapshot" "Development snapshot (apk)" "$state") | |
| for v in "${VERSIONS_LIST[@]}"; do | |
| [ -z "$v" ] && continue | |
| case "$v" in | |
| "$stable_series"*|"$old_series"*) ;; | |
| *) continue ;; | |
| esac | |
| state="OFF" | |
| [ "$prefer" = "$v" ] && state="ON" | |
| menu+=("$v" "$(printf '%-24s' "$(version_label "$v")") ($(pkg_tool_for_version "$v"))" "$state") | |
| count=$((count + 1)) | |
| [ "$count" -ge "$max" ] && break | |
| done | |
| state="OFF" | |
| [ "$prefer" = "custom" ] && state="ON" | |
| menu+=("custom" "Type a version that is not listed" "$state") | |
| # If prefer wasn't snapshot/custom and wasn't in the trimmed list, keep custom handy | |
| # but still default first ON item: if nothing ON, mark stable ON | |
| local has_on=0 i | |
| for ((i = 2; i < ${#menu[@]}; i += 3)); do | |
| [ "${menu[$i]}" = "ON" ] && has_on=1 && break | |
| done | |
| if [ "$has_on" -eq 0 ]; then | |
| for ((i = 0; i < ${#menu[@]}; i += 3)); do | |
| if [ "${menu[$i]}" = "$STABLE_VER" ]; then | |
| menu[$((i + 2))]="ON" | |
| has_on=1 | |
| break | |
| fi | |
| done | |
| fi | |
| [ "$has_on" -eq 0 ] && menu[2]="ON" | |
| whiptail_radiolist "OpenWrt Version" \ | |
| "Pick a listed build, snapshot, or Custom. 25.12+ uses apk; 24.10 and older use opkg." \ | |
| 20 78 "${menu[@]}" | |
| } | |
| select_storage() { | |
| local prefer="$1" | |
| local -a menu | |
| local tag type free state | |
| while read -r line || [ -n "$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}') | |
| state="OFF" | |
| [ -n "$prefer" ] && [ "$tag" = "$prefer" ] && state="ON" | |
| menu+=("$tag" "$(printf 'type %-10s free %s' "$type" "$free")" "$state") | |
| done < <(pvesm status -content rootdir | awk 'NR>1') | |
| [ ${#menu[@]} -eq 0 ] && exit_script 1 "No storage pools found" | |
| [ $((${#menu[@]} / 3)) -eq 1 ] && echo "${menu[0]}" && return | |
| local has_on=0 i | |
| for ((i = 2; i < ${#menu[@]}; i += 3)); do | |
| [ "${menu[$i]}" = "ON" ] && has_on=1 && break | |
| done | |
| [ "$has_on" -eq 0 ] && menu[2]="ON" | |
| whiptail_radiolist "Storage Pools" "Select storage for the container rootfs:" 18 78 "${menu[@]}" | |
| } | |
| detect_network_options() { | |
| BRIDGE_LIST=($(ip link | grep -o 'vmbr[0-9]\+' | sort -u)) | |
| BRIDGE_COUNT=${#BRIDGE_LIST[@]} | |
| local all_devs=$(ip link show | grep -oE '^[0-9]+: ([^:]+):' | awk '{print $2}' | cut -d':' -f1 | grep -vE '^(lo|vmbr|veth|tap|fwbr|fwpr|fwln)') | |
| readarray -t ALL_DEVICES <<<"$all_devs" | |
| local bridged_devs=$(bridge link show | cut -d ":" -f2 | cut -d " " -f2) | |
| readarray -t BRIDGED_DEVICES <<<"$bridged_devs" | |
| UNBRIDGED_DEVICES=() | |
| for dev in "${ALL_DEVICES[@]}"; do | |
| local bridged=false | |
| for bdev in "${BRIDGED_DEVICES[@]}"; do [ "$dev" = "$bdev" ] && bridged=true && break; done | |
| [ "$bridged" = false ] && UNBRIDGED_DEVICES+=("$dev") | |
| done | |
| UNBRIDGED_COUNT=${#UNBRIDGED_DEVICES[@]} | |
| } | |
| select_network_option() { | |
| local type="$1" eth="$2" prefer="$3" | |
| local -a menu=("None" "No network assigned" "OFF") | |
| local b d state | |
| for b in "${BRIDGE_LIST[@]}"; do | |
| state="OFF" | |
| [ "$prefer" = "bridge:$b" ] && state="ON" | |
| menu+=("bridge:$b" "Linux bridge $b" "$state") | |
| done | |
| for d in "${UNBRIDGED_DEVICES[@]}"; do | |
| state="OFF" | |
| [ "$prefer" = "device:$d" ] && state="ON" | |
| menu+=("device:$d" "Host NIC $d" "$state") | |
| done | |
| local has_on=0 i | |
| for ((i = 2; i < ${#menu[@]}; i += 3)); do | |
| [ "${menu[$i]}" = "ON" ] && has_on=1 && break | |
| done | |
| [ "$has_on" -eq 0 ] && menu[2]="ON" | |
| whiptail_radiolist "$type interface ($eth)" \ | |
| "Where should $type / $eth attach? None leaves that interface unconnected." \ | |
| 18 72 "${menu[@]}" | |
| } | |
| detect_next_ctid() { | |
| local id=$(pvesh get /cluster/nextid 2>/dev/null) | |
| echo "${id:-100}" | |
| } | |
| guest_exec() { | |
| pct exec "$CTID" -- /bin/sh -c "$1" | |
| } | |
| wait_for_guest() { | |
| local i | |
| echo -e "${GREEN}Waiting for container userspace...${NC}" | |
| for i in $(seq 1 40); do | |
| if pct exec "$CTID" -- /bin/sh -c 'command -v uci >/dev/null && command -v ubus >/dev/null' >/dev/null 2>&1; then | |
| # give procd a moment to publish objects | |
| pct exec "$CTID" -- /bin/sh -c 'ubus wait_for system 2>/dev/null || true' >/dev/null 2>&1 | |
| return 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo -e "${RED}Container started, but uci is not ready yet β continuing anyway${NC}" | |
| return 1 | |
| } | |
| destroy_container() { | |
| echo -e "${RED}Removing container $CTID...${NC}" | |
| pct stop "$CTID" --timeout 15 >/dev/null 2>&1 || true | |
| if ! pct destroy "$CTID" --purge >/dev/null 2>&1; then | |
| pct destroy "$CTID" >/dev/null 2>&1 || true | |
| fi | |
| echo -e "${RED}Container $CTID removed.${NC}" | |
| } | |
| abort_after_failure() { | |
| local reason="$1" | |
| echo -e "${RED}$reason${NC}" | |
| if whiptail --title "Install failed" --yesno \ | |
| "$reason\n\nDestroy container $CTID ($CTNAME) and abort?\n\nYes = stop + delete the CT\nNo = keep it and continue" \ | |
| 14 74; then | |
| destroy_container | |
| exit_script 1 "Aborted after failure; container $CTID removed" | |
| fi | |
| echo -e "${GREEN}Keeping container $CTID despite the failure${NC}" | |
| } | |
| wait_for_wan() { | |
| local i | |
| echo -e "${GREEN}Waiting for IPv4 default route...${NC}" | |
| for i in $(seq 1 25); do | |
| if pct exec "$CTID" -- /bin/sh -c \ | |
| "ip route show 2>/dev/null | grep -q '^default ' || ip -4 route show 2>/dev/null | grep -q default" \ | |
| >/dev/null 2>&1; then | |
| echo -e "${GREEN}IPv4 WAN is up${NC}" | |
| return 0 | |
| fi | |
| # Kick DHCP once if nothing appeared | |
| [ "$i" -eq 8 ] && guest_exec "ip link set eth0 up 2>/dev/null; udhcpc -i eth0 -n -q -t 3 -T 2 >/dev/null 2>&1 || true" >/dev/null 2>&1 || true | |
| sleep 1 | |
| done | |
| echo -e "${RED}No IPv4 default route in the guest${NC}" | |
| return 1 | |
| } | |
| # Latest <pkg>-<version>.apk name from an OpenWrt feed directory listing | |
| feed_apk_name() { | |
| local repo="$1" pkg="$2" | |
| curl -fsSL --max-time 20 "$repo" 2>/dev/null | | |
| grep -oE "href=\"${pkg}-[0-9][^\"]*\\.apk\"" | | |
| sed 's/^href="//;s/"$//' | | |
| sort -V | tail -1 | |
| } | |
| install_luci_from_host() { | |
| local work repo_luci repo_base name dest f | |
| [ "$PKG_TOOL" = "apk" ] || return 1 | |
| work=$(mktemp -d /tmp/owrt-luci.XXXXXX) || return 1 | |
| dest="/tmp/luci-apks" | |
| repo_luci="https://downloads.openwrt.org/snapshots/packages/x86_64/luci/" | |
| repo_base="https://downloads.openwrt.org/snapshots/packages/x86_64/base/" | |
| echo -e "${GREEN}Downloading LuCI packages on the Proxmox host (guest wget is broken in this LXC)...${NC}" | |
| local luci_pkgs=( | |
| luci luci-light luci-base luci-theme-bootstrap | |
| luci-mod-admin-full luci-mod-network luci-mod-status luci-mod-system | |
| luci-app-package-manager luci-app-firewall luci-lib-uqr | |
| luci-proto-ipv6 luci-proto-ppp | |
| liblucihttp0 liblucihttp-ucode rpcd-mod-luci | |
| ) | |
| local base_pkgs=( | |
| cgi-io uhttpd uhttpd-mod-ubus | |
| rpcd-mod-ucode rpcd-mod-iwinfo rpcd-mod-rrdns | |
| ucode-mod-html ucode-mod-log | |
| libiwinfo-data | |
| ) | |
| for name in "${luci_pkgs[@]}"; do | |
| f=$(feed_apk_name "$repo_luci" "$name") | |
| [ -n "$f" ] || { echo -e "${RED}No apk listed for $name in luci feed${NC}"; rm -rf "$work"; return 1; } | |
| echo " $f" | |
| curl -fsSL --max-time 60 -o "$work/$f" "${repo_luci}${f}" || { rm -rf "$work"; return 1; } | |
| done | |
| for name in "${base_pkgs[@]}"; do | |
| f=$(feed_apk_name "$repo_base" "$name") | |
| [ -n "$f" ] || { echo -e "${RED}No apk listed for $name in base feed${NC}"; rm -rf "$work"; return 1; } | |
| echo " $f" | |
| curl -fsSL --max-time 60 -o "$work/$f" "${repo_base}${f}" || { rm -rf "$work"; return 1; } | |
| done | |
| # SONAME-stamped libiwinfo, e.g. libiwinfo20230701 | |
| f=$(curl -fsSL --max-time 20 "$repo_base" 2>/dev/null | grep -oE 'href="libiwinfo[0-9][^"]*\.apk"' | sed 's/^href="//;s/"$//' | sort -V | tail -1) | |
| if [ -n "$f" ]; then | |
| echo " $f" | |
| curl -fsSL --max-time 60 -o "$work/$f" "${repo_base}${f}" || { rm -rf "$work"; return 1; } | |
| fi | |
| guest_exec "mkdir -p $dest" >/dev/null 2>&1 || true | |
| for f in "$work"/*.apk; do | |
| pct push "$CTID" "$f" "$dest/$(basename "$f")" || { rm -rf "$work"; return 1; } | |
| done | |
| rm -rf "$work" | |
| echo -e "${GREEN}Installing pushed LuCI packages inside the container...${NC}" | |
| pct exec "$CTID" -- /bin/sh -c "apk add --allow-untrusted $dest/*.apk" || return 1 | |
| guest_exec " | |
| [ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd restart >/dev/null 2>&1 || true | |
| [ -x /etc/init.d/uhttpd ] && /etc/init.d/uhttpd restart >/dev/null 2>&1 || true | |
| " >/dev/null 2>&1 || true | |
| return 0 | |
| } | |
| install_luci() { | |
| if wait_for_wan; then | |
| echo -e "${GREEN}Trying in-guest $PKG_TOOL add luci...${NC}" | |
| if [ "$PKG_TOOL" = "apk" ]; then | |
| pct exec "$CTID" -- /bin/sh -c "apk update && apk add luci" && return 0 | |
| else | |
| pct exec "$CTID" -- /bin/sh -c "opkg update && opkg install luci" && return 0 | |
| fi | |
| echo -e "${RED}In-guest package fetch failed β falling back to host download${NC}" | |
| else | |
| echo -e "${RED}Guest has no WAN route β installing LuCI via host download${NC}" | |
| fi | |
| install_luci_from_host | |
| } | |
| valid_mac() { | |
| [[ "$1" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]] | |
| } | |
| load_saved_config() { | |
| SAVED=0 | |
| [ -f "$CONFIG_FILE" ] || return | |
| # shellcheck disable=SC1090 | |
| . "$CONFIG_FILE" || return | |
| SAVED=1 | |
| } | |
| save_config() { | |
| umask 077 | |
| cat > "$CONFIG_FILE" <<EOF | |
| # Last successful OpenWrt LXC settings (password is never stored) | |
| # Generated by install_openwrt_proxmox.sh | |
| SAVED_VER=$(printf '%q' "$VER") | |
| SAVED_RELEASE_TYPE=$(printf '%q' "$RELEASE_TYPE") | |
| SAVED_CTID=$(printf '%q' "$CTID") | |
| SAVED_CTNAME=$(printf '%q' "$CTNAME") | |
| SAVED_MEMORY=$(printf '%q' "$MEMORY") | |
| SAVED_CORES=$(printf '%q' "$CORES") | |
| SAVED_STORAGE_SIZE=$(printf '%q' "$STORAGE_SIZE") | |
| SAVED_STORAGE=$(printf '%q' "$STORAGE") | |
| SAVED_LAN_CIDR=$(printf '%q' "$LAN_CIDR") | |
| SAVED_WAN_OPTION=$(printf '%q' "$WAN_OPTION") | |
| SAVED_LAN_OPTION=$(printf '%q' "$LAN_OPTION") | |
| SAVED_WAN_MAC=$(printf '%q' "$WAN_MAC") | |
| SAVED_LAN_MAC=$(printf '%q' "$LAN_MAC") | |
| SAVED_DISABLE_SYNTPD=$(printf '%q' "$DISABLE_SYNTPD") | |
| SAVED_DISABLE_FIREWALL=$(printf '%q' "$DISABLE_FIREWALL") | |
| SAVED_INSTALL_LUCI=$(printf '%q' "${INSTALL_LUCI:-0}") | |
| EOF | |
| echo -e "${GREEN}Saved settings to $CONFIG_FILE${NC}" | |
| } | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main logic | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| load_saved_config | |
| USE_SAVED=0 | |
| if [ "$SAVED" -eq 1 ]; then | |
| echo -e "${GREEN}Found last-run settings in $CONFIG_FILE${NC}" | |
| if whiptail --title "Reuse last settings?" --yesno \ | |
| "Use saved values as defaults? You can still change any prompt.\n\n Version: ${SAVED_VER:-unknown}\n Name: ${SAVED_CTNAME:-unknown}\n LAN IPv4/CIDR: ${SAVED_LAN_CIDR:-unknown}\n WAN: ${SAVED_WAN_OPTION:-None}\n LAN: ${SAVED_LAN_OPTION:-None}" \ | |
| 16 74; then | |
| USE_SAVED=1 | |
| fi | |
| fi | |
| echo -e "${GREEN}Fetching OpenWrt version info...${NC}" | |
| detect_versions | |
| echo -e "${GREEN}Latest stable: $STABLE_VER${NC}" | |
| echo -e "${GREEN}Old stable: $OLDSTABLE_VER${NC}" | |
| [ -n "$UPCOMING_VER" ] && echo -e "${GREEN}Upcoming: $UPCOMING_VER${NC}" | |
| PREF_VER="$STABLE_VER" | |
| [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_VER" ] && PREF_VER="$SAVED_VER" | |
| SELECTED_VER=$(select_version "$PREF_VER") | |
| if [ "$SELECTED_VER" = "custom" ]; then | |
| CUSTOM_DEFAULT="$STABLE_VER" | |
| [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_VER" ] && [ "$SAVED_VER" != "snapshot" ] && CUSTOM_DEFAULT="$SAVED_VER" | |
| whiptail_input "OpenWrt Version" "Enter version (e.g. $STABLE_VER, $OLDSTABLE_VER, 25.12.0-rc5)" "$CUSTOM_DEFAULT" VER | |
| elif [ "$SELECTED_VER" = "snapshot" ]; then | |
| VER="snapshot" | |
| else | |
| VER="$SELECTED_VER" | |
| fi | |
| if [ "$VER" = "snapshot" ]; then | |
| RELEASE_TYPE="Snapshot" | |
| DOWNLOAD_URL="https://downloads.openwrt.org/snapshots/targets/x86/64/openwrt-x86-64-rootfs.tar.gz" | |
| TEMPLATE_FILE="openwrt-snapshot-${ARCH}.tar.gz" | |
| LUCI_DEFAULT="--yesno" | |
| [ "$USE_SAVED" -eq 1 ] && [ "${SAVED_INSTALL_LUCI:-0}" = "0" ] && LUCI_DEFAULT="--defaultno --yesno" | |
| if whiptail --title "LuCI" $LUCI_DEFAULT "Install LuCI automatically (snapshot)?" 10 60; then | |
| INSTALL_LUCI=1 | |
| else | |
| INSTALL_LUCI=0 | |
| fi || exit_script 1 "Aborted by user" | |
| else | |
| if [ "$(series_of "$VER")" = "$(series_of "$OLDSTABLE_VER")" ] && [ "$(series_of "$VER")" != "$(series_of "$STABLE_VER")" ]; then | |
| RELEASE_TYPE="OldStable" | |
| else | |
| RELEASE_TYPE="Stable" | |
| fi | |
| DOWNLOAD_URL="https://downloads.openwrt.org/releases/$VER/targets/x86/64/openwrt-${VER}-x86-64-rootfs.tar.gz" | |
| TEMPLATE_FILE="openwrt-${VER}-${ARCH}.tar.gz" | |
| INSTALL_LUCI=0 | |
| fi | |
| PKG_TOOL=$(pkg_tool_for_version "$VER") | |
| echo -e "${GREEN}Using $VER ($RELEASE_TYPE, $PKG_TOOL)${NC}" | |
| NEXT_CTID=$(detect_next_ctid) | |
| CTID_DEFAULT="$NEXT_CTID" | |
| if [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_CTID" ]; then | |
| if pct list | awk '{print $1}' | grep -q "^${SAVED_CTID}$"; then | |
| echo -e "${GREEN}Saved CTID $SAVED_CTID is in use β offering $NEXT_CTID${NC}" | |
| else | |
| CTID_DEFAULT="$SAVED_CTID" | |
| fi | |
| fi | |
| whiptail_input "Container ID" "Enter ID" "$CTID_DEFAULT" CTID || exit_script 1 "Aborted" | |
| if [ "$VER" = "snapshot" ] || [ "$RELEASE_TYPE" = "Snapshot" ]; then | |
| NAME_DEFAULT="openwrt-snap-$CTID" | |
| else | |
| NAME_DEFAULT="openwrt-$CTID" | |
| fi | |
| # Keep a previously chosen custom hostname, but never reuse openwrt-<old-id> | |
| if [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_CTNAME" ]; then | |
| case "$SAVED_CTNAME" in | |
| openwrt-[0-9]*|openwrt-snap-[0-9]*) ;; | |
| *) NAME_DEFAULT="$SAVED_CTNAME" ;; | |
| esac | |
| fi | |
| whiptail_input "Container Name" "Enter name" "$NAME_DEFAULT" CTNAME || exit_script 1 "Aborted" | |
| # Password (never saved) | |
| while true; do | |
| PASSWORD=$(whiptail --title "Root Password" --passwordbox "Enter password (blank = skip)" 10 50 3>&1 1>&2 2>&3) | |
| ret=$? | |
| [ $ret -ne 0 ] && { PASSWORD=""; break; } | |
| PASSWORD_CONFIRM=$(whiptail --title "Confirm" --passwordbox "Confirm password" 10 50 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| if [ -z "$PASSWORD" ] && [ -z "$PASSWORD_CONFIRM" ]; then | |
| echo -e "${GREEN}Password skipped.${NC}" | |
| break | |
| elif [ "$PASSWORD" = "$PASSWORD_CONFIRM" ]; then | |
| break | |
| else | |
| whiptail --title "Error" --msgbox "Passwords do not match." 8 50 | |
| fi | |
| done | |
| SYNTPD_ON="ON"; SYNTPD_OFF="OFF" | |
| [ "$USE_SAVED" -eq 1 ] && [ "$SAVED_DISABLE_SYNTPD" = "No" ] && { SYNTPD_ON="OFF"; SYNTPD_OFF="ON"; } | |
| DISABLE_SYNTPD=$(whiptail --title "sysntpd" --radiolist \ | |
| "Disable sysntpd? Recommended for containers (host already keeps time)." 13 74 2 \ | |
| "Yes" "Disable sysntpd (default)" "$SYNTPD_ON" \ | |
| "No" "Leave sysntpd running" "$SYNTPD_OFF" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| FW_DISABLE="OFF"; FW_KEEP="ON" | |
| [ "$USE_SAVED" -eq 1 ] && [ "$SAVED_DISABLE_FIREWALL" = "Yes" ] && { FW_DISABLE="ON"; FW_KEEP="OFF"; } | |
| DISABLE_FIREWALL=$(whiptail --title "Firewall" --radiolist \ | |
| "OpenWrt firewall inside the container?" 13 74 2 \ | |
| "No" "Keep firewall enabled (default)" "$FW_KEEP" \ | |
| "Yes" "Stop and disable it (only if something else filters)" "$FW_DISABLE" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| MEM_DEFAULT="$DEFAULT_MEMORY" | |
| CORES_DEFAULT="$DEFAULT_CORES" | |
| SIZE_DEFAULT="$DEFAULT_STORAGE" | |
| CIDR_DEFAULT="$DEFAULT_LAN_CIDR" | |
| [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_MEMORY" ] && MEM_DEFAULT="$SAVED_MEMORY" | |
| [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_CORES" ] && CORES_DEFAULT="$SAVED_CORES" | |
| [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_STORAGE_SIZE" ] && SIZE_DEFAULT="$SAVED_STORAGE_SIZE" | |
| [ "$USE_SAVED" -eq 1 ] && [ -n "$SAVED_LAN_CIDR" ] && CIDR_DEFAULT="$SAVED_LAN_CIDR" | |
| whiptail_input "Memory (MB)" "Memory size" "$MEM_DEFAULT" MEMORY || exit_script 1 "Aborted" | |
| whiptail_input "CPU Cores" "Number of cores" "$CORES_DEFAULT" CORES || exit_script 1 "Aborted" | |
| whiptail_input "Storage (GB)" "Storage limit" "$SIZE_DEFAULT" STORAGE_SIZE || exit_script 1 "Aborted" | |
| whiptail_input "LAN IPv4/CIDR" \ | |
| "Container LAN address and prefix β not just the network.\nExample: 10.23.45.1/24 means IP 10.23.45.1 with mask 255.255.255.0" \ | |
| "$CIDR_DEFAULT" LAN_CIDR 12 74 || exit_script 1 "Aborted" | |
| # Basic validation | |
| [[ "$CTID" =~ ^[0-9]+$ && "$CTID" -ge 100 ]] || exit_script 1 "ID must be >= 100" | |
| pct list | awk '{print $1}' | grep -q "^$CTID$" && exit_script 1 "ID $CTID in use" | |
| [[ "$MEMORY" =~ ^[0-9]+$ && "$MEMORY" -ge 64 ]] || exit_script 1 "Memory >= 64" | |
| [[ "$CORES" =~ ^[0-9]+$ && "$CORES" -ge 1 ]] || exit_script 1 "Cores >= 1" | |
| [[ "$STORAGE_SIZE" =~ ^[0-9]*\.?[0-9]+$ ]] || exit_script 1 "Storage > 0 required" | |
| awk -v s="$STORAGE_SIZE" 'BEGIN { exit !(s+0 > 0) }' || exit_script 1 "Storage > 0 required" | |
| [[ "$LAN_CIDR" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] || exit_script 1 "LAN IPv4/CIDR must look like 10.23.45.1/24" | |
| LAN_IP=$(echo "$LAN_CIDR" | cut -d'/' -f1) | |
| LAN_PREFIX=$(echo "$LAN_CIDR" | cut -d'/' -f2) | |
| case "$LAN_PREFIX" in | |
| 24) LAN_NETMASK="255.255.255.0" ;; | |
| 23) LAN_NETMASK="255.255.254.0" ;; | |
| 22) LAN_NETMASK="255.255.252.0" ;; | |
| 16) LAN_NETMASK="255.255.0.0" ;; | |
| *) exit_script 1 "Unsupported prefix /$LAN_PREFIX (use /24, /23, /22, or /16)" ;; | |
| esac | |
| STORAGE_PREF="" | |
| [ "$USE_SAVED" -eq 1 ] && STORAGE_PREF="$SAVED_STORAGE" | |
| STORAGE=$(select_storage "$STORAGE_PREF") | |
| detect_network_options | |
| [ "$BRIDGE_COUNT" -eq 0 ] && [ "$UNBRIDGED_COUNT" -eq 0 ] && echo -e "${RED}Warning: No network devices found${NC}" | |
| WAN_PREF=""; LAN_PREF="" | |
| [ "$USE_SAVED" -eq 1 ] && WAN_PREF="$SAVED_WAN_OPTION" | |
| [ "$USE_SAVED" -eq 1 ] && LAN_PREF="$SAVED_LAN_OPTION" | |
| WAN_OPTION=$(select_network_option "WAN" "eth0" "$WAN_PREF") || exit_script 1 "Aborted" | |
| LAN_OPTION=$(select_network_option "LAN" "eth1" "$LAN_PREF") || exit_script 1 "Aborted" | |
| WAN_BRIDGE=""; WAN_DEVICE="" | |
| [[ "$WAN_OPTION" == bridge:* ]] && WAN_BRIDGE="${WAN_OPTION#bridge:}" | |
| [[ "$WAN_OPTION" == device:* ]] && WAN_DEVICE="${WAN_OPTION#device:}" | |
| LAN_BRIDGE=""; LAN_DEVICE="" | |
| [[ "$LAN_OPTION" == bridge:* ]] && LAN_BRIDGE="${LAN_OPTION#bridge:}" | |
| [[ "$LAN_OPTION" == device:* ]] && LAN_DEVICE="${LAN_OPTION#device:}" | |
| WAN_MAC_DEFAULT="" | |
| LAN_MAC_DEFAULT="" | |
| [ "$USE_SAVED" -eq 1 ] && WAN_MAC_DEFAULT="$SAVED_WAN_MAC" | |
| [ "$USE_SAVED" -eq 1 ] && LAN_MAC_DEFAULT="$SAVED_LAN_MAC" | |
| # Device passthrough default: copy host NIC MAC if none saved | |
| if [ -z "$WAN_MAC_DEFAULT" ] && [ -n "$WAN_DEVICE" ]; then | |
| WAN_MAC_DEFAULT=$(ip link show "$WAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2) | |
| fi | |
| if [ -z "$LAN_MAC_DEFAULT" ] && [ -n "$LAN_DEVICE" ]; then | |
| LAN_MAC_DEFAULT=$(ip link show "$LAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2) | |
| fi | |
| whiptail_input "WAN MAC (eth0)" \ | |
| "Hardware address for WAN/eth0.\nLeave blank to let Proxmox assign one." \ | |
| "$WAN_MAC_DEFAULT" WAN_MAC || exit_script 1 "Aborted" | |
| whiptail_input "LAN MAC (eth1)" \ | |
| "Hardware address for LAN/eth1.\nLeave blank to let Proxmox assign one." \ | |
| "$LAN_MAC_DEFAULT" LAN_MAC || exit_script 1 "Aborted" | |
| WAN_MAC="${WAN_MAC,,}" | |
| LAN_MAC="${LAN_MAC,,}" | |
| [ -n "$WAN_MAC" ] && ! valid_mac "$WAN_MAC" && exit_script 1 "Invalid WAN MAC: $WAN_MAC" | |
| [ -n "$LAN_MAC" ] && ! valid_mac "$LAN_MAC" && exit_script 1 "Invalid LAN MAC: $LAN_MAC" | |
| # Summary | |
| SUMMARY="Summary:\n" | |
| SUMMARY+=" Version: $VER ($PKG_TOOL)\n" | |
| SUMMARY+=" ID/Name: $CTID / $CTNAME\n" | |
| SUMMARY+=" Password: $( [ -n "$PASSWORD" ] && echo Set || echo Skipped )\n" | |
| SUMMARY+=" sysntpd: $( [ "$DISABLE_SYNTPD" = "Yes" ] && echo DISABLED || echo Enabled )\n" | |
| SUMMARY+=" firewall: $( [ "$DISABLE_FIREWALL" = "Yes" ] && echo DISABLED || echo Enabled )\n" | |
| SUMMARY+=" Memory/Cores/Storage: $MEMORY MB / $CORES / $STORAGE_SIZE GB on $STORAGE\n" | |
| SUMMARY+=" LAN IPv4/CIDR: $LAN_CIDR\n" | |
| SUMMARY+=" WAN: ${WAN_BRIDGE:-${WAN_DEVICE:-None}} (eth0) MAC ${WAN_MAC:-auto}\n" | |
| SUMMARY+=" LAN: ${LAN_BRIDGE:-${LAN_DEVICE:-None}} (eth1) MAC ${LAN_MAC:-auto}\n" | |
| [ "$RELEASE_TYPE" = "Snapshot" ] && [ "${INSTALL_LUCI:-0}" -eq 1 ] && SUMMARY+=" LuCI: auto-install\n" | |
| whiptail --title "Confirm" --yesno "$SUMMARY\n\nCreate container?" 22 78 \ | |
| || exit_script 0 "Aborted by user" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Template handling | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEMPLATE_PATH="$TEMPLATE_DIR/$TEMPLATE_FILE" | |
| download_needed=1 | |
| if [ -f "$TEMPLATE_PATH" ]; then | |
| FILE_SIZE=$(stat -c %s "$TEMPLATE_PATH" 2>/dev/null || echo 0) | |
| if [ "$FILE_SIZE" -eq 0 ]; then | |
| echo -e "${RED}Existing file empty β redownload${NC}" | |
| rm -f "$TEMPLATE_PATH" | |
| elif ! tar tzf "$TEMPLATE_PATH" >/dev/null 2>&1; then | |
| echo -e "${RED}Corrupt template detected${NC}" | |
| if whiptail --title "Corrupt File" --yesno "Redownload?" 10 60; then | |
| rm -f "$TEMPLATE_PATH" | |
| else | |
| exit_script 1 "Aborted - corrupt file kept" | |
| fi | |
| else | |
| SIZE_H=$(numfmt --to=iec --format %.2f "$FILE_SIZE") | |
| if whiptail --title "Reuse Template?" --yesno \ | |
| "Found:\n $TEMPLATE_FILE\n Size: $SIZE_H\n\nReuse? (No = redownload)" 12 70; then | |
| echo -e "${GREEN}Reusing existing template${NC}" | |
| download_needed=0 | |
| else | |
| rm -f "$TEMPLATE_PATH" | |
| fi | |
| fi | |
| fi | |
| if [ "$RELEASE_TYPE" = "Snapshot" ] && [ "$download_needed" -eq 0 ]; then | |
| FILE_AGE=$(($(date +%s) - $(stat -c %Y "$TEMPLATE_PATH" 2>/dev/null || echo 0))) | |
| if [ "$FILE_AGE" -gt 86400 ]; then | |
| echo -e "${GREEN}Snapshot >1 day old β refreshing${NC}" | |
| rm -f "$TEMPLATE_PATH" | |
| download_needed=1 | |
| fi | |
| fi | |
| if [ "$download_needed" -eq 1 ]; then | |
| echo -e "${GREEN}Downloading $VER rootfs...${NC}" | |
| echo " $DOWNLOAD_URL" | |
| wget --show-progress "$DOWNLOAD_URL" -O "$TEMPLATE_PATH.part" || { | |
| rm -f "$TEMPLATE_PATH.part" | |
| exit_script 1 "Download failed: $DOWNLOAD_URL" | |
| } | |
| mv "$TEMPLATE_PATH.part" "$TEMPLATE_PATH" | |
| if ! tar tzf "$TEMPLATE_PATH" >/dev/null 2>&1; then | |
| rm -f "$TEMPLATE_PATH" | |
| exit_script 1 "Downloaded file corrupt" | |
| fi | |
| echo -e "${GREEN}Download verified${NC}" | |
| fi | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Container creation | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| echo -e "${GREEN}Creating container $CTID...${NC}" | |
| NET_OPTS=() | |
| if [ -n "$WAN_BRIDGE" ]; then | |
| net="name=eth0,bridge=$WAN_BRIDGE" | |
| [ -n "$WAN_MAC" ] && net+=",hwaddr=$WAN_MAC" | |
| NET_OPTS+=("--net0" "$net") | |
| elif [ -n "$WAN_DEVICE" ]; then | |
| net="name=eth0" | |
| if [ -n "$WAN_MAC" ]; then | |
| net+=",hwaddr=$WAN_MAC" | |
| else | |
| net+=",hwaddr=$(ip link show "$WAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)" | |
| fi | |
| NET_OPTS+=("--net0" "$net") | |
| fi | |
| if [ -n "$LAN_BRIDGE" ]; then | |
| net="name=eth1,bridge=$LAN_BRIDGE" | |
| [ -n "$LAN_MAC" ] && net+=",hwaddr=$LAN_MAC" | |
| NET_OPTS+=("--net1" "$net") | |
| elif [ -n "$LAN_DEVICE" ]; then | |
| net="name=eth1" | |
| if [ -n "$LAN_MAC" ]; then | |
| net+=",hwaddr=$LAN_MAC" | |
| else | |
| net+=",hwaddr=$(ip link show "$LAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)" | |
| fi | |
| NET_OPTS+=("--net1" "$net") | |
| fi | |
| pct create "$CTID" "$TEMPLATE_PATH" \ | |
| --arch amd64 \ | |
| --hostname "$CTNAME" \ | |
| --rootfs "$STORAGE:$STORAGE_SIZE" \ | |
| --memory "$MEMORY" \ | |
| --cores "$CORES" \ | |
| --unprivileged 1 \ | |
| --features nesting=1 \ | |
| --ostype unmanaged \ | |
| "${NET_OPTS[@]}" || exit_script 1 "pct create failed" | |
| if ! pct start "$CTID"; then | |
| abort_after_failure "pct start failed for container $CTID." | |
| fi | |
| wait_for_guest | |
| guest_exec "sed -i 's!procd_add_jail!: procd_add_jail!g' /etc/init.d/dnsmasq 2>/dev/null || true" >/dev/null 2>&1 || true | |
| [ "$DISABLE_SYNTPD" = "Yes" ] && { | |
| echo -e "${GREEN}Disabling sysntpd...${NC}" | |
| guest_exec "rm -f /etc/rc.d/*sysntpd" >/dev/null 2>&1 || true | |
| } | |
| [ "$DISABLE_FIREWALL" = "Yes" ] && { | |
| echo -e "${GREEN}Disabling firewall...${NC}" | |
| guest_exec " | |
| /etc/init.d/firewall stop >/dev/null 2>&1 || true | |
| /etc/init.d/firewall disable >/dev/null 2>&1 || true | |
| rm -f /etc/rc.d/*firewall | |
| if /sbin/uci -q get firewall.@defaults[0] >/dev/null; then | |
| /sbin/uci -q set firewall.@defaults[0].input='ACCEPT' | |
| /sbin/uci -q set firewall.@defaults[0].forward='ACCEPT' | |
| /sbin/uci -q set firewall.@defaults[0].output='ACCEPT' | |
| /sbin/uci -q commit firewall | |
| fi | |
| " >/dev/null 2>&1 || echo -e "${RED}Firewall disable warning${NC}" | |
| } | |
| echo -e "${GREEN}Configuring OpenWrt network (eth0=WAN dhcp/dhcpv6, eth1=LAN static)...${NC}" | |
| NETCFG=$(mktemp) | |
| { | |
| printf '%s\n' \ | |
| "config interface 'loopback'" \ | |
| " option device 'lo'" \ | |
| " option proto 'static'" \ | |
| " option ipaddr '127.0.0.1'" \ | |
| " option netmask '255.0.0.0'" \ | |
| "" \ | |
| "config globals 'globals'" \ | |
| " option packet_steering '1'" \ | |
| "" | |
| if [ -n "$LAN_BRIDGE" ] || [ -n "$LAN_DEVICE" ]; then | |
| printf '%s\n' \ | |
| "config device" \ | |
| " option name 'br-lan'" \ | |
| " option type 'bridge'" \ | |
| " list ports 'eth1'" \ | |
| "" \ | |
| "config interface 'lan'" \ | |
| " option device 'br-lan'" \ | |
| " option proto 'static'" \ | |
| " option ipaddr '$LAN_IP'" \ | |
| " option netmask '$LAN_NETMASK'" \ | |
| "" | |
| fi | |
| if [ -n "$WAN_BRIDGE" ] || [ -n "$WAN_DEVICE" ]; then | |
| printf '%s\n' \ | |
| "config interface 'wan'" \ | |
| " option device 'eth0'" \ | |
| " option proto 'dhcp'" \ | |
| "" \ | |
| "config interface 'wan6'" \ | |
| " option device 'eth0'" \ | |
| " option proto 'dhcpv6'" \ | |
| " option reqaddress 'try'" \ | |
| " option reqprefix 'auto'" \ | |
| "" | |
| fi | |
| } > "$NETCFG" | |
| if pct push "$CTID" "$NETCFG" /etc/config/network; then | |
| guest_exec "/etc/init.d/network restart >/dev/null 2>&1 || true" >/dev/null 2>&1 || true | |
| echo -e "${GREEN}Wrote /etc/config/network${NC}" | |
| else | |
| echo -e "${RED}Failed to push /etc/config/network${NC}" | |
| fi | |
| rm -f "$NETCFG" | |
| [ "$RELEASE_TYPE" = "Snapshot" ] && [ "${INSTALL_LUCI:-0}" -eq 1 ] && { | |
| if ! install_luci; then | |
| abort_after_failure "LuCI install failed ($PKG_TOOL could not fetch packages)." | |
| fi | |
| } | |
| [ -n "$PASSWORD" ] && { | |
| echo -e "${GREEN}Setting password...${NC}" | |
| if ! echo -e "$PASSWORD\n$PASSWORD" | pct exec "$CTID" -- passwd; then | |
| abort_after_failure "Setting the root password failed." | |
| fi | |
| } | |
| save_config | |
| echo -e "${GREEN}Done! Container $CTID ($CTNAME) ready.${NC}" | |
| echo "Next:" | |
| echo " pct exec $CTID /bin/sh" | |
| echo " uci show network" | |
| [ "$DISABLE_SYNTPD" = "Yes" ] && echo " sysntpd disabled" | |
| [ "$DISABLE_FIREWALL" = "Yes" ] && echo " firewall disabled" | |
| echo " Settings saved: $CONFIG_FILE" | |
| echo " LuCI: http://$LAN_IP (if LAN is up)" | |
| [ -z "$PASSWORD" ] && echo " Set password: pct exec $CTID passwd" | |
| if [ "$RELEASE_TYPE" = "Snapshot" ] && [ "${INSTALL_LUCI:-0}" -eq 0 ]; then | |
| if [ "$PKG_TOOL" = "apk" ]; then | |
| echo " apk update && apk add luci" | |
| else | |
| echo " opkg update && opkg install luci" | |
| fi | |
| fi | |
| exit 0 |
OpenWrt LXC Creator Script β Changelog
- Added automatic detection of latest stable + newer release candidates (RCs)
- Prompt user if a newer RC exists β option to use it
- When choosing RC, manual version input now defaults to the selected RC version
- Robust template handling: check size + tar integrity, prompt to reuse/redownload, clean up partial/corrupt files
- Force snapshot refresh if >1 day old
- Exit cleanly on Esc/Cancel in all whiptail dialogs
- Improved messages, prompt clarity, and summary display
Now safer, smarter about versions, and friendlier to cancel out of. π
From b53d44d8db59f7f44f60070dafe87a1379ee68a9 Mon Sep 17 00:00:00 2001
From: Alexander Georgievskiy <galeksandrp@gmail.com>
Date: Thu, 21 May 2026 13:36:36 +0300
Subject: [PATCH]
---
install_openwrt_proxmox.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/install_openwrt_proxmox.sh b/install_openwrt_proxmox.sh
index 1fa27aa..d82e270 100644
--- a/install_openwrt_proxmox.sh
+++ b/install_openwrt_proxmox.sh
@@ -57,7 +57,7 @@ whiptail_input() {
detect_latest_stable() {
local ver
ver=$(curl -sSf "https://downloads.openwrt.org/" |
- grep -oP '(?<=OpenWrt )\d+\.\d+\.\d+(?=\s|</strong>|Released)' |
+ grep -oP '(?<=OpenWrt )\d+\.\d+\.\d+(?=\s|</a></strong>|Released)' |
head -1)
if [ -z "$ver" ]; then
ver=$(curl -sSf "https://downloads.openwrt.org/releases/" |
--
2.54.0.windows.1
OpenWrt 25.12.4 is here.
OpenWrt changed the downloads homepage HTML, so the old scrape no longer saw current releases. Versions now come from https://downloads.openwrt.org/.versions.json (stable, old-stable, RCs, snapshot), with a directory-name fallback if that file is missing. You can pick a listed build or type one.
Last successful settings are saved to /root/.openwrt-proxmox-lxc.conf and offered as defaults on the next run (ID/name, storage, bridges, MACs, LAN IPv4/CIDR, memory/cores, sysntpd, firewall, LuCI). The root password is never stored. If the old CTID is taken, the next free ID is offered. Auto hostnames are openwrt-, or openwrt-snap- for snapshots; a custom name is kept.
Guest network is written as a full /etc/config/network:
- eth0 / net0 = WAN, DHCP + DHCPv6
- eth1 / net1 = LAN, br-lan static at the address you enter (field is labeled LAN IPv4/CIDR, e.g. 10.23.45.1/24)
Optional WAN/LAN MACs (aa:bb:cc:dd:ee:ff, blank = Proxmox assigns). Firewall stays enabled by default; you can still disable it. Snapshot LuCI still uses apk; 24.10 uses opkg. If a later step fails you can destroy the new CT and abort.
Tested on snapshot, 25.12.5, and 24.10.8.
I'll be trying this soon and finding out for myself I guess, but how much (if at all) did this break with the ProxMox 9.1 update (November 19th). Also, any thoughts on OpenWRT's release candidate instead of stable? By that I just mean is the RC reasonably reliable and does it run on a more similar linux kernal version to ProxMox 9.1 or not?