rename vendor files to put extension after version string
Flask's send_file uses mimetypes.guess_type() which needs the file extension at the end of the path. Previously files were named acme.sh-3.1.3 and htm.js-3.1.1 — now acme-3.1.3.sh and htm-3.1.1.js. Also: update-vendor.sh now owns ACME_HOME installation, removing duplicate logic from install.sh.
This commit is contained in:
Executable
+495
@@ -0,0 +1,495 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Vacuum Wall - SSL Proxy Firewall Appliance Installer
|
||||||
|
# Run as root on a fresh Debian 13 (trixie) system
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[!!]${NC} $*"; exit 1; }
|
||||||
|
|
||||||
|
# --- CLI argument parsing ---
|
||||||
|
_cli_user=""
|
||||||
|
_cli_is_dev=false
|
||||||
|
_cli_path=""
|
||||||
|
_cli_mgmt_pass=""
|
||||||
|
_cli_mgmt_user=""
|
||||||
|
_cli_mgmt_domain=""
|
||||||
|
_cli_force_venv=false
|
||||||
|
_cli_wan_iface=""
|
||||||
|
_cli_lan_ifaces=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--user|-u) _cli_user="$2"; shift 2 ;;
|
||||||
|
--path|-p) _cli_path="$2"; shift 2 ;;
|
||||||
|
--dev) _cli_is_dev=true; shift ;;
|
||||||
|
--mgmt-pass) _cli_mgmt_pass="$2"; shift 2 ;;
|
||||||
|
--mgmt-user) _cli_mgmt_user="$2"; shift 2 ;;
|
||||||
|
--mgmt-domain) _cli_mgmt_domain="$2"; shift 2 ;;
|
||||||
|
--force-venv) _cli_force_venv=true; shift ;;
|
||||||
|
--wan-iface) _cli_wan_iface="$2"; shift 2 ;;
|
||||||
|
--lan-ifaces) _cli_lan_ifaces="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
printf '%s\n' \
|
||||||
|
"Usage: scripts/install.sh [OPTIONS]" \
|
||||||
|
"" \
|
||||||
|
"Options:" \
|
||||||
|
" --user, -u USER WebUI user (created if it does not exist, required for non-dev mode)" \
|
||||||
|
" --path, -p DIR Install directory (default: repo root)" \
|
||||||
|
" --dev Dev mode: auto-detect repo owner, skip safety warning" \
|
||||||
|
" --mgmt-pass PASS WebUI basic auth password (required)" \
|
||||||
|
" --mgmt-user USER WebUI basic auth username (default: admin)" \
|
||||||
|
" --mgmt-domain DOMAIN Management domain (auto-detected)" \
|
||||||
|
" --wan-iface IFACE WAN interface name (auto-detected)" \
|
||||||
|
" --lan-ifaces IFC,... LAN interface names, comma-separated (auto-detected)" \
|
||||||
|
" -h, --help Show this help" \
|
||||||
|
"" \
|
||||||
|
"All options also have environment variable equivalents:" \
|
||||||
|
" USER_NAME, INSTALL_DIR, MGMT_PASS, MGMT_USER," \
|
||||||
|
" MGMT_DOMAIN, WAN_IFACE, LAN_IFACES." \
|
||||||
|
" CLI flags take precedence over env vars." \
|
||||||
|
"" \
|
||||||
|
"Example (dev):" \
|
||||||
|
" ./scripts/install.sh --dev --mgmt-pass pass" \
|
||||||
|
"" \
|
||||||
|
"Example (prod):" \
|
||||||
|
" MGMT_PASS=pass ./scripts/install.sh --user vacuum-wall"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "Unknown argument: $1 (use --help for usage)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Resolve config: CLI flag > env var > default ---
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")/../" && pwd)"
|
||||||
|
|
||||||
|
# Required settings (no defaults — must be provided)
|
||||||
|
MGMT_PASS="${_cli_mgmt_pass:-${MGMT_PASS:-}}"
|
||||||
|
# Optional settings with defaults
|
||||||
|
MGMT_USER="${_cli_mgmt_user:-${MGMT_USER:-admin}}"
|
||||||
|
|
||||||
|
# MGMT_DOMAIN — CLI > env > auto-detect from hostname
|
||||||
|
if [[ -n "$_cli_mgmt_domain" ]]; then
|
||||||
|
DOMAIN="$_cli_mgmt_domain"
|
||||||
|
elif [[ -n "${MGMT_DOMAIN:-}" ]]; then
|
||||||
|
DOMAIN="$MGMT_DOMAIN"
|
||||||
|
else
|
||||||
|
HOSTNAME_F=$(hostname -f 2>/dev/null || hostname 2>/dev/null || true)
|
||||||
|
if [[ -z "$HOSTNAME_F" ]]; then
|
||||||
|
err "Cannot determine system hostname — set MGMT_DOMAIN env var or --mgmt-domain."
|
||||||
|
fi
|
||||||
|
DOMAIN="${HOSTNAME_F}.local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install directory (CLI > env > repo root)
|
||||||
|
INSTALL_DIR="${_cli_path:-${INSTALL_DIR:-}}"
|
||||||
|
if [[ -n "$INSTALL_DIR" ]]; then
|
||||||
|
PROJECT_DIR="$INSTALL_DIR"
|
||||||
|
else
|
||||||
|
PROJECT_DIR="$REPO_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Network interfaces (CLI > env — auto-detect happens later if still unset)
|
||||||
|
WAN_IFACE="${_cli_wan_iface:-${WAN_IFACE:-}}"
|
||||||
|
LAN_IFACES="${_cli_lan_ifaces:-${LAN_IFACES:-}}"
|
||||||
|
|
||||||
|
# --- Pre-flight checks ---
|
||||||
|
[[ $EUID -eq 0 ]] || err "This script must be run as root."
|
||||||
|
[[ -f /etc/debian_version ]] || warn "This script is designed for Debian/Ubuntu."
|
||||||
|
|
||||||
|
# --- Validate required settings ---
|
||||||
|
missing=()
|
||||||
|
[[ -z "$MGMT_PASS" ]] && missing+=("MGMT_PASS (--mgmt-pass)")
|
||||||
|
|
||||||
|
if (( ${#missing[@]} )); then
|
||||||
|
echo -e "${RED}[!!]${NC} Missing required settings:"
|
||||||
|
for v in "${missing[@]}"; do
|
||||||
|
case "$v" in
|
||||||
|
"MGMT_PASS (--mgmt-pass)") echo ' export MGMT_PASS="your-password" # or --mgmt-pass';;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf '\nTo run: MGMT_PASS=pass ./scripts/install.sh\n'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ACME_HOME="$PROJECT_DIR/data/acme"
|
||||||
|
|
||||||
|
# Dev mode: auto-detect repo owner as service user
|
||||||
|
if [[ "$_cli_is_dev" == true ]]; then
|
||||||
|
_repo_owner=$(stat -c '%U' "$REPO_DIR" 2>/dev/null) || true
|
||||||
|
if [[ -n "$_repo_owner" && "$_repo_owner" != "root" ]]; then
|
||||||
|
_cli_user="$_repo_owner"
|
||||||
|
log "Dev mode: using repo owner '$_repo_owner' as service user"
|
||||||
|
else
|
||||||
|
err "Dev mode: cannot determine repo owner (root or unavailable)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve USER_NAME: dev mode auto-detects, non-dev requires --user
|
||||||
|
USER_NAME="${_cli_user:-${USER_NAME:-}}"
|
||||||
|
if [[ -z "$USER_NAME" ]]; then
|
||||||
|
err "WebUI user is required. Use --dev to auto-detect repo owner, or set --user / USER_NAME."
|
||||||
|
fi
|
||||||
|
# Daemon user name (derived from web UI user name)
|
||||||
|
USER_DAEMON_NAME="${USER_NAME}d"
|
||||||
|
|
||||||
|
# --- Safety check: running service as a regular user ---
|
||||||
|
if [[ "$_cli_is_dev" != true ]] && id "$USER_NAME" &>/dev/null; then
|
||||||
|
_uid=$(id -u "$USER_NAME")
|
||||||
|
_shell=$(getent passwd "$USER_NAME" | cut -d: -f7)
|
||||||
|
if [[ "$_uid" -ge 1000 ]] && [[ "$_shell" != "/usr/sbin/nologin" && "$_shell" != "/bin/false" ]]; then
|
||||||
|
warn "USER_NAME='$USER_NAME' is a regular user (UID=$_uid, shell=$_shell)!"
|
||||||
|
warn "This runs the web service as your login account."
|
||||||
|
warn "Sudo access is held only by the daemon user ($USER_DAEMON_NAME)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Create WebUI user if it does not exist
|
||||||
|
if ! id "$USER_NAME" &>/dev/null; then
|
||||||
|
log "Creating system user $USER_NAME..."
|
||||||
|
useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin "$USER_NAME"
|
||||||
|
fi
|
||||||
|
# Shared group: use the WebUI user's primary group
|
||||||
|
USER_GROUP=$(id -gn "$USER_NAME")
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo " Vacuum Wall Appliance Installer"
|
||||||
|
echo " Install dir: $PROJECT_DIR"
|
||||||
|
echo " WebUI user: $USER_NAME"
|
||||||
|
echo " Daemon user: $USER_DAEMON_NAME"
|
||||||
|
echo " Shared group: $USER_GROUP"
|
||||||
|
echo " Management domain: $DOMAIN"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
# --- 1. Install packages ---
|
||||||
|
log "Installing system packages..."
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq \
|
||||||
|
firewalld \
|
||||||
|
nginx \
|
||||||
|
dnsmasq \
|
||||||
|
wireguard-tools \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
jq \
|
||||||
|
curl \
|
||||||
|
iptables \
|
||||||
|
nftables \
|
||||||
|
apache2-utils \
|
||||||
|
avahi-daemon
|
||||||
|
|
||||||
|
# --- 1b. Vendored libraries ---
|
||||||
|
log "Downloading vendored libraries..."
|
||||||
|
bash "${PROJECT_DIR}/scripts/update-vendor.sh" || err "update-vendor.sh failed"
|
||||||
|
|
||||||
|
# --- 2. Setup users ---
|
||||||
|
log "WebUI user: $USER_NAME (group: $USER_GROUP)"
|
||||||
|
|
||||||
|
# --- 2a. Create daemon user (has sudo for privileged operations) ---
|
||||||
|
if ! id "$USER_DAEMON_NAME" &>/dev/null; then
|
||||||
|
log "Creating system user $USER_DAEMON_NAME..."
|
||||||
|
useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin \
|
||||||
|
--gid "$USER_GROUP" "$USER_DAEMON_NAME"
|
||||||
|
else
|
||||||
|
log "User $USER_DAEMON_NAME already exists."
|
||||||
|
usermod -g "$USER_GROUP" "$USER_DAEMON_NAME" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2b. Setup Python venv ---
|
||||||
|
if [[ -x "${PROJECT_DIR}/.venv/bin/python3" ]] && [[ "$_cli_force_venv" != true ]]; then
|
||||||
|
log "Python venv already exists, skipping (use --force-venv to recreate)."
|
||||||
|
else
|
||||||
|
log "Setting up Python virtual environment..."
|
||||||
|
rm -rf "${PROJECT_DIR}/.venv"
|
||||||
|
python3 -m venv "${PROJECT_DIR}/.venv"
|
||||||
|
"${PROJECT_DIR}/.venv/bin/pip" install -qe "${PROJECT_DIR}"
|
||||||
|
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/.venv"
|
||||||
|
chmod -R g+x "${PROJECT_DIR}/.venv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install the deploy hook into acme.sh's deploy directory
|
||||||
|
# (acme.sh only resolves hooks from $ACME_HOME/deploy/)
|
||||||
|
mkdir -p "$ACME_HOME/deploy"
|
||||||
|
cp "${PROJECT_DIR}/system/acme-deploy.sh" "$ACME_HOME/deploy/acme-deploy.sh"
|
||||||
|
chown "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME/deploy/acme-deploy.sh"
|
||||||
|
chmod 0755 "$ACME_HOME/deploy/acme-deploy.sh"
|
||||||
|
|
||||||
|
# --- 3. Setup directories ---
|
||||||
|
log "Creating config and data directories..."
|
||||||
|
mkdir -p "${PROJECT_DIR}/config"/{dnsmasq,nginx,wireguard,firewall}
|
||||||
|
mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard,acme}
|
||||||
|
mkdir -p /etc/wireguard
|
||||||
|
mkdir -p /etc/dnsmasq
|
||||||
|
# Set ownership: daemon owns project dir in prod, repo owner keeps ownership in dev
|
||||||
|
if [[ "$_cli_is_dev" == true ]]; then
|
||||||
|
_dev_owner="$USER_NAME"
|
||||||
|
else
|
||||||
|
_dev_owner="$USER_DAEMON_NAME"
|
||||||
|
fi
|
||||||
|
chown -R "$_dev_owner:$USER_GROUP" "$PROJECT_DIR"
|
||||||
|
chmod -R g+rwX "$PROJECT_DIR"
|
||||||
|
find "$PROJECT_DIR" -type d -exec chmod g+s '{}' +
|
||||||
|
|
||||||
|
# --- 4. Template rendering function ---
|
||||||
|
# Renders Jinja2 templates by injecting env vars as template context.
|
||||||
|
# Used for systemd units and sudoers files.
|
||||||
|
render_template() {
|
||||||
|
export USER_NAME USER_DAEMON_NAME USER_GROUP PROJECT_DIR ACME_HOME
|
||||||
|
"${PROJECT_DIR}/.venv/bin/python3" -c "
|
||||||
|
import sys, os
|
||||||
|
from jinja2 import Template
|
||||||
|
text = open(sys.argv[1]).read()
|
||||||
|
env = {
|
||||||
|
'USER_NAME': os.environ['USER_NAME'],
|
||||||
|
'USER_DAEMON_NAME': os.environ.get('USER_DAEMON_NAME', ''),
|
||||||
|
'USER_GROUP': os.environ.get('USER_GROUP', ''),
|
||||||
|
'PROJECT_DIR': os.environ['PROJECT_DIR'],
|
||||||
|
'ACME_HOME': os.environ['ACME_HOME'],
|
||||||
|
}
|
||||||
|
print(Template(text).render(**env), end='')
|
||||||
|
" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 5. Install sudoers ---
|
||||||
|
log "Installing sudoers whitelist..."
|
||||||
|
export USER_DAEMON_NAME
|
||||||
|
render_template "${PROJECT_DIR}/system/sudoers.d/vacuum-walld" \
|
||||||
|
| install -m 0440 /dev/stdin /etc/sudoers.d/vacuum-walld
|
||||||
|
|
||||||
|
visudo -cf /etc/sudoers.d/vacuum-walld || err "Invalid sudoers file!"
|
||||||
|
|
||||||
|
# --- 6. Install systemd units ---
|
||||||
|
log "Installing systemd units..."
|
||||||
|
export USER_GROUP
|
||||||
|
render_template "${PROJECT_DIR}/system/systemd/vacuum-walld.service" \
|
||||||
|
| install -m 0644 /dev/stdin /etc/systemd/system/vacuum-walld.service
|
||||||
|
|
||||||
|
render_template "${PROJECT_DIR}/system/systemd/vacuum-wall.service" \
|
||||||
|
| install -m 0644 /dev/stdin /etc/systemd/system/vacuum-wall.service
|
||||||
|
|
||||||
|
render_template "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.service" \
|
||||||
|
| install -m 0644 /dev/stdin /etc/systemd/system/vacuum-wall-acme.service
|
||||||
|
|
||||||
|
install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.timer" /etc/systemd/system/vacuum-wall-acme.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# --- 7. Enable IP forwarding (persistent via sysctl.conf) ---
|
||||||
|
log "Enabling IP forwarding..."
|
||||||
|
if ! grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null; then
|
||||||
|
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 8. Detect network interfaces ---
|
||||||
|
log "Detecting network interfaces..."
|
||||||
|
|
||||||
|
# Auto-detect WAN (interface with default gateway)
|
||||||
|
WAN_IFACE="${WAN_IFACE:-}"
|
||||||
|
if [[ -z "$WAN_IFACE" ]]; then
|
||||||
|
# Strip @if<port> suffix — physical port index can change on reboot
|
||||||
|
DETECTED_WAN=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}' | cut -d'@' -f1)
|
||||||
|
if [[ -n "$DETECTED_WAN" ]]; then
|
||||||
|
WAN_IFACE="$DETECTED_WAN"
|
||||||
|
log "Auto-detected WAN interface: $WAN_IFACE"
|
||||||
|
else
|
||||||
|
warn "Could not auto-detect WAN interface — set WAN_IFACE env var"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-detect LAN (all non-loopback, non-Docker, non-WAN, non-virtual interfaces)
|
||||||
|
LAN_IFACES="${LAN_IFACES:-}"
|
||||||
|
if [[ -z "$LAN_IFACES" ]]; then
|
||||||
|
DETECTED_LANS=$(ls /sys/class/net/ 2>/dev/null \
|
||||||
|
| grep -vE "^(lo|docker|br-|virbr|${WAN_IFACE})$")
|
||||||
|
if [[ -n "$DETECTED_LANS" ]]; then
|
||||||
|
LAN_IFACES=$(echo "$DETECTED_LANS" | paste -sd ',' -)
|
||||||
|
log "Auto-detected LAN interfaces: $LAN_IFACES"
|
||||||
|
else
|
||||||
|
warn "Could not auto-detect LAN interfaces — set LAN_IFACES env var"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 9. Enable and start core services ---
|
||||||
|
log "Enabling services..."
|
||||||
|
systemctl enable firewalld >/dev/null 2>&1 && log "Enabled firewalld" || warn "Could not enable firewalld"
|
||||||
|
systemctl enable nginx >/dev/null 2>&1 && log "Enabled nginx" || warn "Could not enable nginx"
|
||||||
|
systemctl enable dnsmasq >/dev/null 2>&1 && log "Enabled dnsmasq" || warn "Could not enable dnsmasq"
|
||||||
|
systemctl enable vacuum-walld >/dev/null 2>&1 && log "Enabled vacuum-walld" || warn "Could not enable vacuum-walld"
|
||||||
|
systemctl enable vacuum-wall >/dev/null 2>&1 && log "Enabled vacuum-wall" || warn "Could not enable vacuum-wall"
|
||||||
|
systemctl enable vacuum-wall-acme.timer >/dev/null 2>&1 && log "Enabled vacuum-wall-acme.timer" || warn "Could not enable vacuum-wall-acme.timer"
|
||||||
|
systemctl enable avahi-daemon >/dev/null 2>&1 && log "Enabled avahi-daemon" || warn "Could not enable avahi-daemon"
|
||||||
|
|
||||||
|
# Clean up old nginx bootstrap configs (replaced by daemon-generated config)
|
||||||
|
rm -f /etc/nginx/conf.d/vacuum-wall-map.conf /etc/nginx/conf.d/vacuum-wall-mgmt.conf
|
||||||
|
|
||||||
|
# Stop all services to ensure clean start order
|
||||||
|
systemctl stop vacuum-wall >/dev/null 2>&1 || true
|
||||||
|
systemctl stop vacuum-walld >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Start services in dependency order
|
||||||
|
systemctl start firewalld >/dev/null 2>&1 && log "Started firewalld" || warn "Could not start firewalld"
|
||||||
|
systemctl start avahi-daemon >/dev/null 2>&1 && log "Started avahi-daemon" || warn "Could not start avahi-daemon"
|
||||||
|
systemctl start dnsmasq >/dev/null 2>&1 || warn "Could not start dnsmasq (no interfaces configured yet)"
|
||||||
|
|
||||||
|
# Start daemon and wait for socket
|
||||||
|
systemctl start vacuum-walld >/dev/null 2>&1 && log "Started vacuum-walld daemon" || warn "Could not start vacuum-walld daemon"
|
||||||
|
|
||||||
|
_SOCKET="$PROJECT_DIR/data/daemon.sock"
|
||||||
|
for _i in $(seq 1 30); do
|
||||||
|
[[ -S "$_SOCKET" ]] && break
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
if [[ ! -S "$_SOCKET" ]]; then
|
||||||
|
warn "Daemon socket not found at $_SOCKET — skipping API configuration"
|
||||||
|
else
|
||||||
|
chown "$USER_DAEMON_NAME:$USER_GROUP" "$_SOCKET" 2>/dev/null || true
|
||||||
|
chmod 0660 "$_SOCKET" 2>/dev/null || true
|
||||||
|
|
||||||
|
# --- 10. Configure subsystems via daemon API ---
|
||||||
|
log "Configuring subsystems via daemon API..."
|
||||||
|
WAN_IFACE="$WAN_IFACE" \
|
||||||
|
LAN_IFACES="$LAN_IFACES" \
|
||||||
|
MGMT_DOMAIN="$DOMAIN" \
|
||||||
|
MGMT_USER="$MGMT_USER" \
|
||||||
|
MGMT_PASS="$MGMT_PASS" \
|
||||||
|
"${PROJECT_DIR}/.venv/bin/python3" -c "
|
||||||
|
import daemon.client as c
|
||||||
|
from daemon.iface import (
|
||||||
|
POST_ACME_SELF_SIGNED, POST_NGINX_DOMAINS_ADD, POST_NGINX_APPLY,
|
||||||
|
POST_FIREWALL_CONFIG, POST_FIREWALL_CONFIG_APPLY, POST_NETWORK_SYSCTL_SET,
|
||||||
|
GET_NETWORK_INFER_DHCP_RANGES,
|
||||||
|
)
|
||||||
|
import sys
|
||||||
|
|
||||||
|
domain = '${DOMAIN}'
|
||||||
|
mgmt_user = '${MGMT_USER}'
|
||||||
|
mgmt_pass = '${MGMT_PASS}'
|
||||||
|
wan_iface = '${WAN_IFACE}'
|
||||||
|
lan_ifaces = '${LAN_IFACES}'
|
||||||
|
|
||||||
|
# Self-signed cert for management domain
|
||||||
|
try:
|
||||||
|
res = c.post(POST_ACME_SELF_SIGNED, {'domain': domain, 'days': 365})
|
||||||
|
print(f' [cert] Self-signed: {\"generated\" if res.get(\"generated\") else \"exists\"}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' [cert] Warning: {e}', file=sys.stderr)
|
||||||
|
|
||||||
|
# Management proxy domain + htpasswd
|
||||||
|
try:
|
||||||
|
c.post(POST_NGINX_DOMAINS_ADD, {
|
||||||
|
'domain': domain,
|
||||||
|
'paths': {
|
||||||
|
'/': {
|
||||||
|
'backend': {'host': '127.0.0.1', 'port': 9090, 'proto': 'http'},
|
||||||
|
'is_management': True,
|
||||||
|
},
|
||||||
|
'/ws': {
|
||||||
|
'backend': {'host': '127.0.0.1', 'port': 9091, 'proto': 'http'},
|
||||||
|
'is_websocket': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'auth_user': mgmt_user,
|
||||||
|
'auth_pass': mgmt_pass,
|
||||||
|
})
|
||||||
|
c.post(POST_NGINX_APPLY)
|
||||||
|
print(f' [proxy] Management proxy configured for {domain}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' [proxy] Warning: {e}', file=sys.stderr)
|
||||||
|
|
||||||
|
# Firewall config (interface detection done in bash above)
|
||||||
|
import json as _json
|
||||||
|
zones = {}
|
||||||
|
|
||||||
|
if wan_iface:
|
||||||
|
zones['public'] = {
|
||||||
|
'target': 'DEFAULT',
|
||||||
|
'interfaces': [i for i in wan_iface.split(',') if i],
|
||||||
|
'services': ['http', 'https', 'ssh'],
|
||||||
|
'masquerade': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if lan_ifaces:
|
||||||
|
zones['internal'] = {
|
||||||
|
'target': 'ACCEPT',
|
||||||
|
'interfaces': [i for i in lan_ifaces.split(',') if i],
|
||||||
|
'services': ['dhcp', 'dns', 'ntp'],
|
||||||
|
'masquerade': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Always create vpn zone skeleton for later WireGuard setup
|
||||||
|
zones['vpn'] = {
|
||||||
|
'target': 'ACCEPT',
|
||||||
|
'interfaces': [],
|
||||||
|
'services': [],
|
||||||
|
'masquerade': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
c.post(POST_FIREWALL_CONFIG, {'zones': zones})
|
||||||
|
c.post(POST_FIREWALL_CONFIG_APPLY)
|
||||||
|
print(' [firewall] Zones configured and applied')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' [firewall] Warning: {e}', file=sys.stderr)
|
||||||
|
|
||||||
|
# IP forwarding
|
||||||
|
try:
|
||||||
|
c.post(POST_NETWORK_SYSCTL_SET, {'name': 'net.ipv4.ip_forward', 'value': '1'})
|
||||||
|
print(' [network] IP forwarding enabled')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' [network] Warning: {e}', file=sys.stderr)
|
||||||
|
|
||||||
|
# Infer DHCP ranges (logged for user reference)
|
||||||
|
try:
|
||||||
|
ranges = c.get(GET_NETWORK_INFER_DHCP_RANGES)
|
||||||
|
for iface, rng in ranges.get('ranges', {}).items():
|
||||||
|
print(f' [suggestion] DHCP range for {iface}: {rng.get(\"start\")}-{rng.get(\"end\")}')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
"
|
||||||
|
log "Subsystem configuration complete"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI"
|
||||||
|
|
||||||
|
nginx -t 2>/dev/null && nginx -s reload 2>/dev/null && log "Reloaded nginx" || \
|
||||||
|
systemctl restart nginx >/dev/null 2>&1 && log "Restarted nginx" || \
|
||||||
|
warn "Could not restart nginx (check config)"
|
||||||
|
|
||||||
|
# --- Done ---
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo -e " ${GREEN}Vacuum Wall installed successfully!${NC}"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
echo " Management UI: https://$DOMAIN"
|
||||||
|
echo " User: $MGMT_USER"
|
||||||
|
echo " Daemon service: vacuum-walld.service"
|
||||||
|
echo " WebUI service: vacuum-wall.service"
|
||||||
|
echo " ACME renewal: vacuum-wall-acme.timer"
|
||||||
|
echo ""
|
||||||
|
echo " Firewall zones:"
|
||||||
|
if [[ -n "$WAN_IFACE" ]]; then
|
||||||
|
echo " public (WAN) → $WAN_IFACE"
|
||||||
|
else
|
||||||
|
echo " public (WAN) → not assigned"
|
||||||
|
fi
|
||||||
|
if [[ -n "$LAN_IFACES" ]]; then
|
||||||
|
echo " internal (LAN) → $LAN_IFACES"
|
||||||
|
else
|
||||||
|
echo " internal (LAN) → not assigned"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo " Next steps:"
|
||||||
|
echo " 1. Register your ACME account at https://$DOMAIN/certs"
|
||||||
|
echo " 2. Verify zone assignments at https://$DOMAIN/interfaces"
|
||||||
|
echo " 3. Configure DHCP ranges for your LAN"
|
||||||
|
echo " 4. Add proxy domains with ACME certificates"
|
||||||
|
echo " 5. Set up WireGuard tunnel (optional)"
|
||||||
|
echo ""
|
||||||
|
echo " NOTE: A self-signed certificate was generated."
|
||||||
|
echo " From the WebUI, issue a real certificate for $DOMAIN"
|
||||||
|
echo " when DNS points to this appliance."
|
||||||
|
echo ""
|
||||||
+52
-23
@@ -1,54 +1,83 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Download and vendor libraries into vendor/.
|
# Download and vendor libraries into vendor/.
|
||||||
# Run from the project root after updating the VERSION variables below.
|
# Files are named {pkg}-{ver}.{ext}, with {pkg}.{ext} symlinks for stable references.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
VENDOR="$PROJECT_DIR/vendor"
|
||||||
|
WEBUI_VENDOR="$PROJECT_DIR/webui/static/vendor"
|
||||||
|
|
||||||
# ---- Library versions ----
|
# ---- Library versions ----
|
||||||
ACME_VERSION="3.1.3"
|
ACME_VERSION="3.1.3"
|
||||||
HTM_VERSION="3.1.1"
|
HTM_VERSION="3.1.1"
|
||||||
|
|
||||||
VENDOR="vendor"
|
# ---- Helpers ----
|
||||||
|
|
||||||
download() {
|
download() {
|
||||||
local name="$1" url="$2" dest="$3"
|
local name="$1" url="$2" dest="$3"
|
||||||
if [[ -n "${SKIP_DOWNLOAD:-}" ]]; then
|
if [[ -n "${SKIP_DOWNLOAD:-}" ]]; then
|
||||||
echo "[skip] $name (SKIP_DOWNLOAD is set)"
|
echo "[skip] $name"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -f "$dest" ]]; then
|
||||||
|
echo "[skip] $name (already present)"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo "[download] $name → $dest"
|
echo "[download] $name → $dest"
|
||||||
curl -sfL -o "$dest" "$url"
|
curl -sfL -o "$dest" "$url"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_symlink() {
|
||||||
|
local link="$1" target="$2"
|
||||||
|
if [[ -L "$link" ]]; then
|
||||||
|
cur=$(readlink "$link")
|
||||||
|
if [[ "$cur" != "$target" ]]; then
|
||||||
|
echo "[symlink] $link → $target (updated)"
|
||||||
|
ln -sf "$target" "$link"
|
||||||
|
fi
|
||||||
|
elif [[ -e "$link" ]]; then
|
||||||
|
echo "[symlink] $link (replacing existing file)"
|
||||||
|
mv -f "$link" "${link}.bak" && ln -sf "$target" "$link"
|
||||||
|
else
|
||||||
|
echo "[symlink] $link → $target"
|
||||||
|
ln -sf "$target" "$link"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- acme.sh ----
|
||||||
|
ACME_VERSIONED="$VENDOR/acme-${ACME_VERSION}.sh"
|
||||||
|
ACME_SYMLINK="$VENDOR/acme.sh"
|
||||||
download "acme.sh@${ACME_VERSION}" \
|
download "acme.sh@${ACME_VERSION}" \
|
||||||
"https://raw.githubusercontent.com/acmesh-official/acme.sh/${ACME_VERSION}/acme.sh" \
|
"https://raw.githubusercontent.com/acmesh-official/acme.sh/${ACME_VERSION}/acme.sh" \
|
||||||
"${VENDOR}/acme.sh"
|
"$ACME_VERSIONED"
|
||||||
|
chmod +x "$ACME_VERSIONED"
|
||||||
|
ensure_symlink "$ACME_SYMLINK" "acme-${ACME_VERSION}.sh"
|
||||||
|
|
||||||
|
# ---- htm ----
|
||||||
|
HTM_VERSIONED="$VENDOR/htm-${HTM_VERSION}.js"
|
||||||
|
HTM_SYMLINK="$VENDOR/htm.js"
|
||||||
download "htm@${HTM_VERSION}" \
|
download "htm@${HTM_VERSION}" \
|
||||||
"https://raw.githubusercontent.com/developit/htm/${HTM_VERSION}/mini/index.module.js" \
|
"https://cdn.jsdelivr.net/npm/htm@${HTM_VERSION}/mini/index.module.js" \
|
||||||
"${VENDOR}/htm.js"
|
"$HTM_VERSIONED"
|
||||||
|
ensure_symlink "$HTM_SYMLINK" "htm-${HTM_VERSION}.js"
|
||||||
|
|
||||||
chmod +x "${VENDOR}/acme.sh"
|
# ---- ACME_HOME (acme.sh runtime home) ----
|
||||||
|
ACME_HOME="${ACME_HOME:-$PROJECT_DIR/data/acme}"
|
||||||
|
mkdir -p "$ACME_HOME"
|
||||||
|
if [[ ! -x "$ACME_HOME/acme.sh" ]]; then
|
||||||
|
cp "$ACME_SYMLINK" "$ACME_HOME/acme.sh"
|
||||||
|
chmod +x "$ACME_HOME/acme.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Symlinks for webui ---
|
# ---- Webui symlinks (point to versioned files) ----
|
||||||
WEBUI_VENDOR="webui/static/vendor"
|
|
||||||
mkdir -p "$WEBUI_VENDOR"
|
mkdir -p "$WEBUI_VENDOR"
|
||||||
|
|
||||||
WEBUI_LINKS=(
|
WEBUI_LINKS=(
|
||||||
"htm.js:../../../vendor/htm.js"
|
"htm.js:../../../vendor/htm-${HTM_VERSION}.js"
|
||||||
)
|
)
|
||||||
|
|
||||||
for entry in "${WEBUI_LINKS[@]}"; do
|
for entry in "${WEBUI_LINKS[@]}"; do
|
||||||
IFS=':' read -r name target <<< "$entry"
|
IFS=':' read -r name target <<< "$entry"
|
||||||
if [[ -L "${WEBUI_VENDOR}/${name}" ]]; then
|
ensure_symlink "${WEBUI_VENDOR}/${name}" "$target"
|
||||||
cur=$(readlink "${WEBUI_VENDOR}/${name}")
|
|
||||||
if [[ "$cur" != "$target" ]]; then
|
|
||||||
echo "[symlink] ${WEBUI_VENDOR}/${name} → ${target} (updated)"
|
|
||||||
ln -sf "$target" "${WEBUI_VENDOR}/${name}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[symlink] ${WEBUI_VENDOR}/${name} → ${target}"
|
|
||||||
ln -sf "$target" "${WEBUI_VENDOR}/${name}"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "[done] All libraries vendored."
|
echo "[done] All libraries vendored."
|
||||||
|
|||||||
Reference in New Issue
Block a user