-
-
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 |
Hello @7thgenerationdesign, I tried hard, even pasting all commands in shell to see if it worked, but there's some problem with wget the image on the board. It fails. I eventually managed to install owrt as a privileged CT, but with a seriously convoluted procedure I might not be able to tell you in depth. I hadto do it manually though, despite the faith I had toward this script! All I can tell you is that it is doable, spending nights to address very weird errors. π¬
Thank you for the script! Btw, AI said that the script is not malicious, well structured and directory agnostic :)
The script worked for me after fixing one issue: storage selection was being cut off (off screen) so I changed one line to hard-code width to 78:
whiptail_radiolist "Storage Pools" "Which storage pool for the ${label,,}?\nUse Spacebar to select." 16 78 "${menu[@]}"
Thanks for your script !
Is your luci/admin/status/realtime/connections working ?
That doesn't look like it's working, I believe there is a kernel module mismatch between proxmox kernel modules and some openwrt expects, namely proc netfilter.
Nice, compared to the linuxcontainers.org builds this has some clear config advantages regarding the network defaults - but I think sysntpd service removal should be implemented here also to avoid conflicts between container and host?
i.e., rm -f /etc/rc.d/*sysntpd
Nice, compared to the linuxcontainers.org builds this has some clear config advantages regarding the network defaults - but I think sysntpd service removal should be implemented here also to avoid conflicts between container and host? i.e.,
rm -f /etc/rc.d/*sysntpd
Good idea! I added a prompt to disable it during the setup. It could still be useful if the OpenWrt is a NTP server, so I made it an option. Additionally, I simplified the installation process by using whiptail for all the prompts to maintain consistency in the user interface.
Is anybody else also having the issue that the lists or pages in luci/admin/status/realtime/connections are empty?
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?
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.
Since I do not have an ARM system to test it on, I don't know.