Skip to content

Instantly share code, notes, and snippets.

@jaminmc
Last active April 1, 2025 05:48
Show Gist options
  • Save jaminmc/7e786a8947746439f7b8a8e2726e629d to your computer and use it in GitHub Desktop.
Save jaminmc/7e786a8947746439f7b8a8e2726e629d to your computer and use it in GitHub Desktop.
Install OpenWrt in a Container on Proxmox 8+!
#!/bin/bash
# Script to create an OpenWrt LXC container in Proxmox
# Downloads from openwrt.org with latest stable or snapshot version, detects bridges/devices, IDs, configures network, sets optional password
# Pre-configures WAN/LAN in UCI, includes summary and confirmation, optional LuCI install for snapshots with apk
# Default resource values
DEFAULT_MEMORY="256" # MB
DEFAULT_CORES="2" # CPU cores
DEFAULT_STORAGE="0.5" # GB
DEFAULT_SUBNET="10.23.45.1/24" # LAN subnet
ARCH="x86_64" # Architecture
TEMPLATE_DIR="/var/lib/vz/template/cache" # Default template location
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
# Exit handler for cleanup and messages
exit_script() {
local code=$1
local msg=$2
[ -n "$msg" ] && echo -e "${RED}$msg${NC}"
exit "$code"
}
# Check if running as root
[ "$EUID" -ne 0 ] && exit_script 1 "Error: This script must be run as root"
# Check required tools
for cmd in wget pct pvesm ip curl whiptail pvesh bridge stat; do
command -v "$cmd" &>/dev/null || exit_script 1 "Error: $cmd is not installed. Please install it first."
done
# Generic whiptail radiolist function
whiptail_radiolist() {
local title="$1" prompt="$2" height="$3" width="$4" items=("${@:5}")
local selection
selection=$(whiptail --title "$title" --radiolist "$prompt" "$height" "$width" "$((${#items[@]} / 3))" "${items[@]}" 3>&1 1>&2 2>&3) || \
exit_script 1 "Error: $title selection aborted"
echo "$selection"
}
# Detect latest stable OpenWrt version (silent)
detect_latest_version() {
local ver
ver=$(curl -sSf "https://downloads.openwrt.org/releases/" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1)
[ -z "$ver" ] && ver="24.10.0" # Default to 24.10.0 if detection fails
echo "$ver"
}
# Select storage
select_storage() {
local content='rootdir' label='Container'
local -a menu
while read -r line || [ -n "$line" ]; do
local tag=$(echo "$line" | awk '{print $1}')
local type=$(echo "$line" | awk '{printf "%-10s", $2}')
local free=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf "%9sB", $6}')
menu+=("$tag" "Type: $type Free: $free" "OFF")
done < <(pvesm status -content "$content" | awk 'NR>1')
[ ${#menu[@]} -eq 0 ] && exit_script 1 "Error: No storage pools found for $label"
[ $((${#menu[@]} / 3)) -eq 1 ] && echo "${menu[0]}" && return
whiptail_radiolist "Storage Pools" "Which storage pool for the ${label,,}?\nUse Spacebar to select." 16 $(( $(echo "${menu[*]}" | wc -L) + 23 )) "${menu[@]}"
}
# Detect network options (bridges and unbridged devices)
detect_network_options() {
BRIDGE_LIST=($(ip link | grep -o 'vmbr[0-9]\+' | sort -u))
BRIDGE_COUNT=${#BRIDGE_LIST[@]}
local all_devs
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
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
bridged=false
for bridged_dev in "${BRIDGED_DEVICES[@]}"; do
[ "$dev" = "$bridged_dev" ] && bridged=true && break
done
[ "$bridged" = false ] && UNBRIDGED_DEVICES+=("$dev")
done
UNBRIDGED_COUNT=${#UNBRIDGED_DEVICES[@]}
}
# Select network option
select_network_option() {
local type="$1" eth="$2"
local -a menu=("None" "No network assigned" "OFF")
for bridge in "${BRIDGE_LIST[@]}"; do
menu+=("bridge:$bridge" "Bridge $bridge" "OFF")
done
for device in "${UNBRIDGED_DEVICES[@]}"; do
menu+=("device:$device" "Device $device" "OFF")
done
whiptail_radiolist "$type Network Selection" "Select a bridge or device for $type ($eth) or 'None':\nUse Spacebar to select." 16 60 "${menu[@]}"
}
# Detect next available Container ID
detect_next_ctid() {
local id
id=$(pvesh get /cluster/nextid)
echo "${id:-100}"
}
# Prompt with default value
prompt_with_default() {
local prompt="$1" default="$2" var="$3"
read -e -p "$prompt (default: $default): " -i "$default" input
eval "$var=\"${input:-$default}\""
}
# Main execution
echo -e "${GREEN}Fetching latest stable OpenWrt version...${NC}"
STABLE_VER=$(detect_latest_version)
echo -e "${GREEN}Detected latest stable version: $STABLE_VER${NC}"
# Select OpenWrt release type
RELEASE_TYPE=$(whiptail --title "OpenWrt Release Type" --radiolist \
"Choose the OpenWrt release type (Stable allows manual version input):\nUse Spacebar to select." 10 60 2 \
"Stable" "Stable version (e.g., $STABLE_VER)" "ON" \
"Snapshot" "Latest daily snapshot" "OFF" 3>&1 1>&2 2>&3) || exit_script 1 "Error: Release type selection aborted"
if [ "$RELEASE_TYPE" = "Stable" ]; then
prompt_with_default "Enter OpenWrt stable version" "$STABLE_VER" VER
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"
else
VER="snapshot"
DOWNLOAD_URL="https://downloads.openwrt.org/snapshots/targets/x86/64/openwrt-x86-64-rootfs.tar.gz"
TEMPLATE_FILE="openwrt-snapshot-$ARCH.tar.gz"
# Prompt for LuCI installation
if whiptail --title "Install LuCI" --yesno "Would you like to automatically install LuCI (graphical web interface) for the snapshot?" 10 60 3>&1 1>&2 2>&3; then
INSTALL_LUCI=1
else
INSTALL_LUCI=0
fi
fi
NEXT_CTID=$(detect_next_ctid)
prompt_with_default "Enter Container ID" "$NEXT_CTID" CTID
prompt_with_default "Enter Container Name" "openwrt-$CTID" CTNAME
while true; do
read -s -p "Enter root password (leave blank to skip): " PASSWORD; echo
read -s -p "Confirm root password: " PASSWORD_CONFIRM; echo
if [ -z "$PASSWORD" ] && [ -z "$PASSWORD_CONFIRM" ]; then
echo -e "${GREEN}Root password skipped.${NC}"
break
elif [ "$PASSWORD" = "$PASSWORD_CONFIRM" ]; then
break
else
echo -e "${RED}Passwords do not match. Please try again.${NC}"
fi
done
prompt_with_default "Enter memory size in MB" "$DEFAULT_MEMORY" MEMORY
prompt_with_default "Enter number of CPU cores" "$DEFAULT_CORES" CORES
prompt_with_default "Enter storage limit in GB" "$DEFAULT_STORAGE" STORAGE_SIZE
prompt_with_default "Enter LAN subnet" "$DEFAULT_SUBNET" SUBNET
# Validate inputs
[[ "$CTID" =~ ^[0-9]+$ && "$CTID" -ge 100 ]] || exit_script 1 "Error: Container ID must be a number >= 100"
pct list | awk '{print $1}' | grep -q "^$CTID$" && exit_script 1 "Error: Container ID $CTID is already in use"
[[ "$MEMORY" =~ ^[0-9]+$ && "$MEMORY" -ge 64 ]] || exit_script 1 "Error: Memory size must be a number >= 64 MB"
[[ "$CORES" =~ ^[0-9]+$ && "$CORES" -ge 1 ]] || exit_script 1 "Error: Core count must be a number >= 1"
[[ "$STORAGE_SIZE" =~ ^[0-9]*\.?[0-9]+$ && "$(echo "$STORAGE_SIZE > 0" | bc)" -eq 1 ]] || exit_script 1 "Error: Storage limit must be a positive number"
# Parse subnet
LAN_IP=$(echo "$SUBNET" | cut -d'/' -f1)
LAN_PREFIX=$(echo "$SUBNET" | 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 "Error: Unsupported subnet prefix /$LAN_PREFIX. Use /16, /22, /23, or /24" ;;
esac
STORAGE=$(select_storage container)
detect_network_options
[ "$BRIDGE_COUNT" -eq 0 ] && [ "$UNBRIDGED_COUNT" -eq 0 ] && echo -e "${RED}Warning: No network options found. Selecting 'None' for WAN/LAN.${NC}"
WAN_OPTION=$(select_network_option "WAN" "eth0")
LAN_OPTION=$(select_network_option "LAN" "eth1")
WAN_BRIDGE=""; WAN_DEVICE=""
if [[ "$WAN_OPTION" == bridge:* ]]; then
WAN_BRIDGE="${WAN_OPTION#bridge:}"
elif [[ "$WAN_OPTION" == device:* ]]; then
WAN_DEVICE="${WAN_OPTION#device:}"
fi
LAN_BRIDGE=""; LAN_DEVICE=""
if [[ "$LAN_OPTION" == bridge:* ]]; then
LAN_BRIDGE="${LAN_OPTION#bridge:}"
elif [[ "$LAN_OPTION" == device:* ]]; then
LAN_DEVICE="${LAN_OPTION#device:}"
fi
# Summary and confirmation
SUMMARY="Container Configuration Summary:\n"
SUMMARY+=" OpenWrt Version: $VER\n"
SUMMARY+=" Container ID: $CTID\n"
SUMMARY+=" Container Name: $CTNAME\n"
SUMMARY+=" Root Password: $( [ -n "$PASSWORD" ] && echo "Set" || echo "Not set" )\n"
SUMMARY+=" Memory: $MEMORY MB\n"
SUMMARY+=" CPU Cores: $CORES\n"
SUMMARY+=" Storage: $STORAGE_SIZE GB on $STORAGE\n"
SUMMARY+=" LAN Subnet: $SUBNET\n"
SUMMARY+=" WAN Interface: ${WAN_BRIDGE:-${WAN_DEVICE:-None}} (eth0, DHCP/DHCPv6)\n"
SUMMARY+=" LAN Interface: ${LAN_BRIDGE:-${LAN_DEVICE:-None}} (eth1, static)\n"
[ "$RELEASE_TYPE" = "Snapshot" ] && [ "$INSTALL_LUCI" -eq 1 ] && SUMMARY+=" LuCI: Will be installed automatically\n"
whiptail --title "Confirm Container Creation" --yesno "$SUMMARY\nProceed with container creation?" 20 60 || exit_script 0 "Container creation aborted by user"
# Download template with snapshot age check
if [ ! -f "$TEMPLATE_DIR/$TEMPLATE_FILE" ]; then
echo -e "${GREEN}Downloading OpenWrt $VER rootfs...${NC}"
wget -q "$DOWNLOAD_URL" -O "$TEMPLATE_DIR/$TEMPLATE_FILE" || exit_script 1 "Error: Failed to download OpenWrt $VER image"
else
if [ "$RELEASE_TYPE" = "Snapshot" ]; then
# Check if snapshot file is older than 1 day (86400 seconds)
FILE_AGE=$(($(date +%s) - $(stat -c %Y "$TEMPLATE_DIR/$TEMPLATE_FILE")))
if [ "$FILE_AGE" -gt 86400 ]; then
echo -e "${GREEN}Snapshot is older than 1 day, refreshing...${NC}"
rm -f "$TEMPLATE_DIR/$TEMPLATE_FILE"
wget -q "$DOWNLOAD_URL" -O "$TEMPLATE_DIR/$TEMPLATE_FILE" || exit_script 1 "Error: Failed to download OpenWrt snapshot"
else
echo -e "${GREEN}Using existing OpenWrt snapshot: $TEMPLATE_FILE${NC}"
fi
else
echo -e "${GREEN}Using existing OpenWrt image: $TEMPLATE_FILE${NC}"
fi
fi
# Build pct create command with corrected network options
echo -e "${GREEN}Creating LXC container $CTID...${NC}"
NET_OPTS=()
[ -n "$WAN_BRIDGE" ] && NET_OPTS+=("--net0" "name=eth0,bridge=$WAN_BRIDGE")
[ -n "$WAN_DEVICE" ] && NET_OPTS+=("--net0" "name=eth0,hwaddr=$(ip link show "$WAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)")
[ -n "$LAN_BRIDGE" ] && NET_OPTS+=("--net1" "name=eth1,bridge=$LAN_BRIDGE")
[ -n "$LAN_DEVICE" ] && NET_OPTS+=("--net1" "name=eth1,hwaddr=$(ip link show "$LAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)")
pct create "$CTID" "$TEMPLATE_DIR/$TEMPLATE_FILE" \
--arch amd64 \
--hostname "$CTNAME" \
--rootfs "$STORAGE:$STORAGE_SIZE" \
--memory "$MEMORY" \
--cores "$CORES" \
--unprivileged 1 \
--features nesting=1 \
--ostype unmanaged \
"${NET_OPTS[@]}" || exit_script 1 "Error: Failed to create container"
echo -e "${GREEN}Starting container $CTID...${NC}"
pct start "$CTID" || exit_script 1 "Error: Failed to start container"
pct exec "$CTID" -- sh -c "sed -i 's!procd_add_jail!: procd_add_jail!g' /etc/init.d/dnsmasq"
sleep 10
echo -e "${GREEN}Configuring network...${NC}"
pct exec "$CTID" -- sh -c "
# Configure WAN (eth0) with DHCP and DHCPv6
uci set network.wan=interface
uci set network.wan.proto='dhcp'
uci set network.wan.device='eth0'
uci set network.wan6=interface
uci set network.wan6.proto='dhcpv6'
uci set network.wan6.device='eth0'
# Configure LAN (eth1) with static IP
uci set network.lan=interface
uci set network.lan.proto='static'
uci set network.@device[0].ports='eth1'
uci set network.lan.ipaddr='$LAN_IP'
uci set network.lan.netmask='$LAN_NETMASK'
# Commit changes and restart network
uci commit network
/etc/init.d/network restart" || echo -e "${RED}Warning: Network configuration failed${NC}"
if [ "$RELEASE_TYPE" = "Snapshot" ] && [ "$INSTALL_LUCI" -eq 1 ]; then
echo -e "${GREEN}Waiting 15 seconds for internet connectivity...${NC}"
sleep 15
echo -e "${GREEN}Installing LuCI...${NC}"
pct exec "$CTID" -- sh -c "apk update; apk add luci" || echo -e "${RED}Warning: LuCI installation failed${NC}"
fi
[ -n "$PASSWORD" ] && {
echo -e "${GREEN}Setting root password...${NC}"
echo -e "$PASSWORD\n$PASSWORD" | pct exec "$CTID" -- passwd || echo -e "${RED}Warning: Failed to set root password${NC}"
} || echo -e "${GREEN}Root password not set (left blank).${NC}"
echo -e "${GREEN}Container $CTID ($CTNAME) created and started!${NC}"
echo "Next steps:"
echo "1. Access: pct exec $CTID /bin/sh"
echo "2. Verify network: uci show network"
if [ "$RELEASE_TYPE" = "Snapshot" ]; then
echo "3. Update: apk update"
if [ "$INSTALL_LUCI" -eq 1 ]; then
echo "4. LuCI installed: Access at http://$LAN_IP (if LAN configured)"
else
echo "4. Install LuCI: apk add luci"
[ -n "$LAN_BRIDGE" ] || [ -n "$LAN_DEVICE" ] && echo "5. LuCI: http://$LAN_IP" || echo "5. Add eth1 to activate LAN: http://$LAN_IP"
fi
else
echo "3. Update: opkg update"
echo "4. Install LuCI: opkg install luci"
[ -n "$LAN_BRIDGE" ] || [ -n "$LAN_DEVICE" ] && echo "5. LuCI: http://$LAN_IP" || echo "5. Add eth1 to activate LAN: http://$LAN_IP"
fi
[ -z "$PASSWORD" ] && echo "6. Set password if needed: pct exec $CTID passwd"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment