diff --git a/install.sh b/install.sh index c0972a4..dc0269a 100755 --- a/install.sh +++ b/install.sh @@ -12,12 +12,12 @@ log() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[!!]${NC} $*"; } err() { echo -e "${RED}[!!]${NC} $*"; exit 1; } -PROJECT_DIR="/home/wall/vacuum-wall" -USER_NAME="vacuum-wall" -USER_HOME="/home/$USER_NAME" +# --- Configurable via environment --- +REPO_DIR="$(cd "$(dirname "$0")" && pwd)" +USER_NAME="${USER_NAME:-vacuum-wall}" + # --- Validate required env vars --- missing=() -[[ -z "${MGMT_DOMAIN:-}" ]] && missing+=(MGMT_DOMAIN) [[ -z "${MGMT_PASS:-}" ]] && missing+=(MGMT_PASS) [[ -z "${ACME_EMAIL:-}" ]] && missing+=(ACME_EMAIL) @@ -25,24 +25,66 @@ if (( ${#missing[@]} )); then echo -e "${RED}[!!]${NC} Missing required environment variables:" for v in "${missing[@]}"; do case "$v" in - MGMT_DOMAIN) echo ' export MGMT_DOMAIN="wall.lan" # Management UI hostname';; MGMT_PASS) echo ' export MGMT_PASS="your-password" # WebUI basic auth password';; - ACME_EMAIL) echo " export ACME_EMAIL=\"you@example.com\" # Let's Encrypt email";; + ACME_EMAIL) echo " export ACME_EMAIL=\"you@example.com\" # ACME (ZeroSSL) registration email";; esac done - printf '\nTo run: MGMT_DOMAIN=wall.lan MGMT_PASS=pass ACME_EMAIL=you@example.com ./install.sh\n' + printf '\nTo run: MGMT_PASS=pass ACME_EMAIL=you@example.com ./install.sh\n' exit 1 fi -DOMAIN="$MGMT_DOMAIN" +# Auto-detect MGMT_DOMAIN from system hostname if not provided +if [[ -z "${MGMT_DOMAIN:-}" ]]; then + 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." + fi + DOMAIN="${HOSTNAME_F}.local" +else + DOMAIN="$MGMT_DOMAIN" +fi MGMT_USER="${MGMT_USER:-admin}" # --- 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." +# --- Deploy to /opt/vacuum-wall --- +INSTALL_DIR="/opt/vacuum-wall" + +# Install rsync first if not available (needed for deploy) +if ! command -v rsync &>/dev/null; then + apt-get update -qq + apt-get install -y -qq rsync +fi + +if [[ -d "$INSTALL_DIR" ]]; then + if [[ -L "$INSTALL_DIR" ]]; then + log "Symbolic link already exists at $INSTALL_DIR, skipping deploy." + elif [[ "$INSTALL_DIR" == "$REPO_DIR" ]]; then + log "Installed from repo location, skipping deploy." + else + err "Installation directory $INSTALL_DIR already exists." + fi +else + log "Deploying $REPO_DIR → $INSTALL_DIR" + rsync -a --delete \ + --exclude='.venv' \ + --exclude='__pycache__' \ + --exclude='*.pyc' \ + --exclude='.git' \ + --exclude='build' \ + "$REPO_DIR/" "$INSTALL_DIR/" + chown -R "$USER_NAME:$USER_NAME" "$INSTALL_DIR" +fi + +PROJECT_DIR="$INSTALL_DIR" +ACME_HOME="$PROJECT_DIR/data/acme" + echo "============================================" echo " Vacuum Wall Appliance Installer" +echo " Install dir: $PROJECT_DIR" +echo " System user: $USER_NAME" echo " Management domain: $DOMAIN" echo "============================================" @@ -60,110 +102,133 @@ apt-get install -y -qq \ curl \ iptables \ nftables \ - apache2-utils + apache2-utils \ + avahi-daemon -# --- 1b. Setup Python venv --- -log "Setting up Python virtual environment..." -python3 -m venv "${PROJECT_DIR}/.venv" -"${PROJECT_DIR}/.venv/bin/pip" install -q "${PROJECT_DIR}" -chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/.venv" - -# --- 2. Create system user --- +# --- 2. Create system user (home set to PROJECT_DIR for env, no-create) --- if ! id "$USER_NAME" &>/dev/null; then log "Creating system user $USER_NAME..." - useradd --system --home-dir "$USER_HOME" --shell /usr/sbin/nologin "$USER_NAME" + useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin "$USER_NAME" else log "User $USER_NAME already exists." fi -# --- 2b. Install acme.sh as the vacuum-wall user --- -if [[ ! -d "$USER_HOME/.acme.sh" ]]; then - log "Installing acme.sh for $USER_NAME..." - su -s /bin/sh "$USER_NAME" -c \ - "ACME_HOME='$USER_HOME/.acme.sh' curl -sS https://get.acme.sh | sh" +# --- 2b. Setup Python venv --- +log "Setting up Python virtual environment..." +python3 -m venv "${PROJECT_DIR}/.venv" +"${PROJECT_DIR}/.venv/bin/pip" install -q "${PROJECT_DIR}" + +# --- 2c. Install acme.sh --- +if [[ ! -d "$ACME_HOME" ]]; then + log "Installing acme.sh..." + mkdir -p "$ACME_HOME" + chown "$USER_NAME:$USER_NAME" "$ACME_HOME" + sudo -u "$USER_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \ + sh -c 'curl -sS https://get.acme.sh | sh' else log "acme.sh already installed." fi -# Ensure the vacuum-wall user owns the acme.sh directory -chown -R "$USER_NAME:$USER_NAME" "$USER_HOME/.acme.sh" - # Ensure the acme deploy hook script has correct permissions chmod 0755 "${PROJECT_DIR}/system/acme-deploy.sh" # --- 3. Setup directories --- -log "Creating data directories..." -mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard} -mkdir -p "$USER_HOME/vacuum-wall" +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 -chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data" -chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/lib" -chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/webui" -chown -R "$USER_NAME:$USER_NAME" "$USER_HOME/vacuum-wall" +# --- 4. Template rendering function --- +render_template() { + export USER_NAME 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'], + 'PROJECT_DIR': os.environ['PROJECT_DIR'], + 'ACME_HOME': os.environ['ACME_HOME'], +} +print(Template(text).render(**env), end='') +" "$1" +} -# --- 4. Install sudoers --- +# --- 5. Install sudoers --- log "Installing sudoers whitelist..." -install -m 0440 "${PROJECT_DIR}/system/sudoers.d/vacuum-wall" /etc/sudoers.d/vacuum-wall +render_template "${PROJECT_DIR}/system/sudoers.d/vacuum-wall" \ + | install -m 0440 /dev/stdin /etc/sudoers.d/vacuum-wall -# Validate sudoers syntax visudo -cf /etc/sudoers.d/vacuum-wall || err "Invalid sudoers file!" -# --- 5. Enable IP forwarding --- +# --- 6. Install systemd units --- +log "Installing systemd units..." +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 --- 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 sysctl -w net.ipv4.ip_forward=1 2>/dev/null || warn "Could not enable IP forwarding (may need kernel access)" -# --- 6. Start and configure firewalld --- +# --- 8. Start and configure firewalld --- log "Enabling firewalld..." -systemctl enable firewalld || warn "Could not enable firewalld (already running?)" -systemctl start firewalld || warn "Could not start firewalld (may need D-Bus)" +systemctl enable firewalld >/dev/null 2>&1 || warn "Could not enable firewalld (already running?)" +systemctl start firewalld >/dev/null 2>&1 || warn "Could not start firewalld (may need D-Bus)" -# Allow management access (HTTP/HTTPS for the proxy) -firewall-cmd --permanent --add-service=http 2>/dev/null || true -firewall-cmd --permanent --add-service=https 2>/dev/null || true -firewall-cmd --permanent --add-service=ssh 2>/dev/null || true -firewall-cmd --reload 2>/dev/null || true +firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true +log "Added service http to public zone" +firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true +log "Added service https to public zone" +firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 || true +log "Added service ssh to public zone" +firewall-cmd --reload >/dev/null 2>&1 || true +log "Firewalld rules reloaded" -# --- 7. Configure dnsmasq --- +# --- 9. Configure dnsmasq --- log "Configuring dnsmasq..." -systemctl enable dnsmasq || true -systemctl start dnsmasq || warn "Could not start dnsmasq (no interfaces configured yet)" +systemctl enable dnsmasq >/dev/null 2>&1 || true +systemctl start dnsmasq >/dev/null 2>&1 || warn "Could not start dnsmasq (no interfaces configured yet)" +log "dnsmasq configured (will fully start after DHCP ranges are set)" -# --- 8. Setup nginx management proxy --- +# --- 10. Setup nginx management proxy --- log "Generating self-signed certificate for management domain..." -mkdir -p "$USER_HOME/.acme.sh/$DOMAIN" +mkdir -p "$ACME_HOME/$DOMAIN" openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout "$USER_HOME/.acme.sh/$DOMAIN/$DOMAIN.key" \ - -out "$USER_HOME/.acme.sh/$DOMAIN/fullchain.cer" \ + -keyout "$ACME_HOME/$DOMAIN/$DOMAIN.key" \ + -out "$ACME_HOME/$DOMAIN/fullchain.cer" \ -subj "/CN=$DOMAIN" \ -addext "subjectAltName=DNS:$DOMAIN" -chown -R "$USER_NAME:$USER_NAME" "$USER_HOME/.acme.sh" +chown -R "$USER_NAME:$USER_NAME" "$ACME_HOME" -# Generate htpasswd (credentials passed via environment to avoid shell injection) -mkdir -p "$USER_HOME/vacuum-wall" -htpasswd -cb "$USER_HOME/vacuum-wall/.htpasswd" "$MGMT_USER" "$MGMT_PASS" 2>/dev/null || \ - MGMT_USER="$MGMT_USER" MGMT_PASS="$MGMT_PASS" python3 -c " +# Generate htpasswd directly in data/nginx/ +htpasswd -cb "${PROJECT_DIR}/data/nginx/.htpasswd" "$MGMT_USER" "$MGMT_PASS" 2>/dev/null || \ + MGMT_USER="$MGMT_USER" MGMT_PASS="$MGMT_PASS" HTFILE="${PROJECT_DIR}/data/nginx/.htpasswd" python3 -c " import os, crypt, base64 password = os.environ['MGMT_PASS'] user = os.environ['MGMT_USER'] salt = '\$6\$' + base64.b64encode(os.urandom(16)).decode().rstrip('=')[:16] hashed = crypt.crypt(password, salt) -with open('$USER_HOME/vacuum-wall/.htpasswd', 'w') as f: +with open(os.environ['HTFILE'], 'w') as f: f.write(user + ':' + hashed + '\n') " 2>/dev/null || \ warn "Could not generate htpasswd (install apache2-utils or python3-crypt)" -chown "$USER_NAME:$USER_NAME" "$USER_HOME/vacuum-wall/.htpasswd" 2>/dev/null +chown "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data/nginx/.htpasswd" -# Copy .htpasswd to the path expected by the running app (data/nginx/.htpasswd) -cp "$USER_HOME/vacuum-wall/.htpasswd" "${PROJECT_DIR}/data/nginx/.htpasswd" 2>/dev/null || true -chown "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data/nginx/.htpasswd" 2>/dev/null || true +# Remove default nginx site so vacuum-wall management config takes precedence +rm -f /etc/nginx/sites-enabled/default # Write WebSocket upgrade map (nginx conf.d/ is already inside http {} context) cat > /etc/nginx/conf.d/vacuum-wall-map.conf <<'MAPEOF' @@ -189,15 +254,15 @@ server { listen 443 ssl; server_name $DOMAIN; - ssl_certificate $USER_HOME/.acme.sh/$DOMAIN/fullchain.cer; - ssl_certificate_key $USER_HOME/.acme.sh/$DOMAIN/$DOMAIN.key; + ssl_certificate $ACME_HOME/$DOMAIN/fullchain.cer; + ssl_certificate_key $ACME_HOME/$DOMAIN/$DOMAIN.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; auth_basic "Vacuum Wall"; - auth_basic_user_file $USER_HOME/vacuum-wall/.htpasswd; + auth_basic_user_file ${PROJECT_DIR}/data/nginx/.htpasswd; location / { proxy_pass http://127.0.0.1:9090; @@ -214,44 +279,169 @@ server { } MGMTSITEEOF -# --- 9. Install systemd units --- -log "Installing systemd units..." -install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall.service" /etc/systemd/system/vacuum-wall.service -install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.service" /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 +# --- 11. Write initial nginx config.json --- +log "Writing initial nginx configuration..." +MGMT_DOMAIN="$DOMAIN" \ +MGMT_USER="$MGMT_USER" \ +INSTALL_DIR="$PROJECT_DIR" \ +"${PROJECT_DIR}/.venv/bin/python3" -c " +import json, os +d = os.environ['MGMT_DOMAIN'] +u = os.environ['MGMT_USER'] +p = os.environ['INSTALL_DIR'] +cfg = { + 'domains': {}, + 'management': { + 'domain': d, + 'backend': { + 'host': '127.0.0.1', + 'port': 9090, + 'proto': 'http' + }, + 'auth': { + 'user': u, + 'htpasswd': p + '/data/nginx/.htpasswd' + } + }, + 'ssl': { + 'protocols': 'TLSv1.2 TLSv1.3', + 'ciphers': 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305', + 'prefer_server_ciphers': False + } +} +with open(os.path.join(p, 'config/nginx/config.json'), 'w') as f: + json.dump(cfg, f, indent=4) + f.write('\n') +" -# --- 10. Setup initial firewalld zones --- -log "Setting up default firewalld zones..." +# --- 12. Auto-detect interfaces and setup initial firewalld zones --- +log "Detecting network interfaces..." -# Internal zone (LAN) -firewall-cmd --permanent --new-zone=internal 2>/dev/null || true -firewall-cmd --permanent --zone=internal --set-target=ACCEPT 2>/dev/null || true -firewall-cmd --permanent --zone=internal --add-service=dhcp 2>/dev/null || true -firewall-cmd --permanent --zone=internal --add-service=dns 2>/dev/null || true -firewall-cmd --permanent --zone=internal --add-service=ntp 2>/dev/null || true +# Auto-detect WAN (interface with default gateway) +WAN_IFACE="${WAN_IFACE:-}" +if [[ -z "$WAN_IFACE" ]]; then + DETECTED_WAN=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}') + 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 -# VPN zone -firewall-cmd --permanent --new-zone=vpn 2>/dev/null || true -firewall-cmd --permanent --zone=vpn --set-target=ACCEPT 2>/dev/null || true +# 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 -# Public/external zone defaults are fine +# Generate config/firewall/config.json +log "Writing initial firewall configuration..." +WAN_IFACE="$WAN_IFACE" \ +LAN_IFACES="$LAN_IFACES" \ +INSTALL_DIR="$PROJECT_DIR" \ +"${PROJECT_DIR}/.venv/bin/python3" -c " +import json, os -firewall-cmd --reload 2>/dev/null || true +wan = os.environ.get('WAN_IFACE', '').strip() or None +lan = os.environ.get('LAN_IFACES', '').strip() or None +p = os.environ['INSTALL_DIR'] -# --- 11. Enable and start services --- +cfg = {'zones': {}} + +if wan: + cfg['zones']['public'] = { + 'target': 'DEFAULT', + 'interfaces': [i for i in wan.split(',') if i], + 'services': ['http', 'https', 'ssh'], + 'masquerade': True, + } + +if lan: + cfg['zones']['internal'] = { + 'target': 'ACCEPT', + 'interfaces': [i for i in lan.split(',') if i], + 'services': ['dhcp', 'dns', 'ntp'], + 'masquerade': False, + } + +# Always create vpn zone skeleton for later WireGuard setup +cfg['zones']['vpn'] = { + 'target': 'ACCEPT', + 'interfaces': [], + 'services': [], + 'masquerade': False, +} + +with open(os.path.join(p, 'config/firewall/config.json'), 'w') as f: + json.dump(cfg, f, indent=2) + f.write('\n') +" + +# Apply zones via firewall-cmd (Python venv not yet fully available for apply_config) +firewall-cmd --permanent --new-zone=internal >/dev/null 2>&1 || true +log "Created firewalld zone: internal" +firewall-cmd --permanent --zone=internal --set-target=ACCEPT >/dev/null 2>&1 || true +firewall-cmd --permanent --zone=internal --add-service=dhcp >/dev/null 2>&1 || true +log "Added service dhcp to internal zone" +firewall-cmd --permanent --zone=internal --add-service=dns >/dev/null 2>&1 || true +log "Added service dns to internal zone" +firewall-cmd --permanent --zone=internal --add-service=ntp >/dev/null 2>&1 || true +log "Added service ntp to internal zone" + +firewall-cmd --permanent --new-zone=vpn >/dev/null 2>&1 || true +log "Created firewalld zone: vpn" +firewall-cmd --permanent --zone=vpn --set-target=ACCEPT >/dev/null 2>&1 || true + +# Apply masquerade on public/WAN +if [[ -n "$WAN_IFACE" ]]; then + firewall-cmd --permanent --zone=public --add-masquerade >/dev/null 2>&1 || true + log "Enabled masquerade on public zone ($WAN_IFACE)" + firewall-cmd --permanent --zone=public --add-interface="$WAN_IFACE" >/dev/null 2>&1 || true + log "Assigned $WAN_IFACE to public zone" +fi + +# Assign LAN interfaces to internal zone +if [[ -n "$LAN_IFACES" ]]; then + IFS=',' read -ra LAN_ARRAY <<< "$LAN_IFACES" + for iface in "${LAN_ARRAY[@]}"; do + iface=$(echo "$iface" | xargs) + [[ -z "$iface" ]] && continue + firewall-cmd --permanent --zone=internal --add-interface="$iface" >/dev/null 2>&1 || true + log "Assigned $iface to internal zone" + done +fi + +firewall-cmd --reload >/dev/null 2>&1 || true +log "Firewalld rules reloaded" + +# --- 13. Enable and start services --- log "Enabling services..." -systemctl enable nginx -systemctl enable vacuum-wall -systemctl enable vacuum-wall-acme.timer +systemctl enable nginx >/dev/null 2>&1 || true +log "Enabled nginx" +systemctl enable vacuum-wall >/dev/null 2>&1 || true +log "Enabled vacuum-wall" +systemctl enable vacuum-wall-acme.timer >/dev/null 2>&1 || true +log "Enabled vacuum-wall-acme.timer" -systemctl start nginx 2>/dev/null || warn "Could not start nginx (check config)" -systemctl start vacuum-wall 2>/dev/null || warn "Could not start vacuum-wall WebUI" +systemctl enable avahi-daemon >/dev/null 2>&1 || true +log "Enabled avahi-daemon" +systemctl start avahi-daemon >/dev/null 2>&1 && log "Started avahi-daemon" || warn "Could not start avahi-daemon" -# --- 12. Configure acme.sh default email --- +systemctl start nginx >/dev/null 2>&1 && log "Started nginx" || warn "Could not start nginx (check config)" +systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI" + +# --- 14. Configure acme.sh default email --- log "Configuring acme.sh default email..." -su -s /bin/sh "$USER_NAME" -c \ - "$USER_HOME/.acme.sh/acme.sh --register-account -m '$ACME_EMAIL'" 2>/dev/null || \ +sudo -u "$USER_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \ + "$ACME_HOME/acme.sh" --register-account -m "$ACME_EMAIL" 2>/dev/null || \ warn "Could not register acme.sh account (will be done from WebUI)" # --- Done --- @@ -265,10 +455,22 @@ echo " User: $MGMT_USER" 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. Assign interfaces to zones from the WebUI" +echo " 1. Verify zone assignments at https://$DOMAIN/interfaces" echo " 2. Configure DHCP ranges for your LAN" -echo " 3. Add proxy domains with Let's Encrypt certs" +echo " 3. Add proxy domains with ACME certificates" echo " 4. Set up WireGuard tunnel (optional)" echo "" echo " NOTE: A self-signed certificate was generated."