#!/usr/bin/env bash # VPS 一键部署:sing-box + Web 管理面板 # 用法:sudo bash scripts/install.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$SCRIPT_DIR")" ENV_FILE="${ROOT_DIR}/.env" RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' log() { echo -e "${GREEN}[+]${NC} $*"; } err() { echo -e "${RED}[!]${NC} $*" >&2; exit 1; } wait_for_apt() { local i=0 while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do if (( i == 0 )); then log "等待 apt 锁释放(系统自动更新中)..." fi (( i++ )) || true if (( i > 120 )); then err "apt 锁等待超时,请稍后重试: bash scripts/install.sh" fi sleep 5 done } [[ $EUID -eq 0 ]] || err "请使用 root 运行: sudo bash scripts/install.sh" [[ -f "$ENV_FILE" ]] || err "缺少 .env 文件,请先: cp .env.example .env 并填写" # shellcheck disable=SC1090 source "$ENV_FILE" : "${VPS_IP:?请在 .env 中设置 VPS_IP}" : "${DOMAIN:?请在 .env 中设置 DOMAIN}" : "${ACME_EMAIL:?请在 .env 中设置 ACME_EMAIL}" : "${REALITY_SERVER_NAME:=www.microsoft.com}" : "${PANEL_USERNAME:=admin}" if [[ -z "${REALITY_PRIVATE_KEY:-}" ]]; then log "未检测到 Reality 密钥,运行 generate-keys.sh ..." bash "$SCRIPT_DIR/generate-keys.sh" source "$ENV_FILE" fi if [[ -z "${PANEL_PASSWORD:-}" ]]; then PANEL_PASSWORD="$(sing-box generate rand --base64 32 | tr -d '/+=' | head -c 20)" if grep -q "^PANEL_PASSWORD=" "$ENV_FILE" 2>/dev/null; then sed -i "s|^PANEL_PASSWORD=.*|PANEL_PASSWORD=${PANEL_PASSWORD}|" "$ENV_FILE" else echo "PANEL_PASSWORD=${PANEL_PASSWORD}" >> "$ENV_FILE" fi source "$ENV_FILE" fi : "${REALITY_PRIVATE_KEY:?}" : "${REALITY_PUBLIC_KEY:?}" : "${REALITY_SHORT_ID:?}" : "${PANEL_PASSWORD:?}" if [[ -z "${CLASH_API_SECRET:-}" ]]; then CLASH_API_SECRET="$(openssl rand -hex 16)" if grep -q "^CLASH_API_SECRET=" "$ENV_FILE" 2>/dev/null; then sed -i "s|^CLASH_API_SECRET=.*|CLASH_API_SECRET=${CLASH_API_SECRET}|" "$ENV_FILE" else echo "CLASH_API_SECRET=${CLASH_API_SECRET}" >> "$ENV_FILE" fi source "$ENV_FILE" fi normalize_panel_path() { local p="${1:-}" p="${p#/}" p="${p%/}" echo "$p" } PANEL_PATH="$(normalize_panel_path "${PANEL_PATH:-}")" if [[ -z "$PANEL_PATH" ]]; then PANEL_PATH="jiedian-$(openssl rand -hex 4)" if grep -q "^PANEL_PATH=" "$ENV_FILE" 2>/dev/null; then sed -i "s|^PANEL_PATH=.*|PANEL_PATH=${PANEL_PATH}|" "$ENV_FILE" else echo "PANEL_PATH=${PANEL_PATH}" >> "$ENV_FILE" fi fi PANEL_LOCATION="/${PANEL_PATH}/" PANEL_PREFIX="/${PANEL_PATH}" PANEL_ALLOW_BLOCK="" if [[ -n "${PANEL_ALLOW_IP:-}" ]]; then PANEL_ALLOW_BLOCK=" allow ${PANEL_ALLOW_IP}; deny all;" fi export JIEDIAN_ROOT="$ROOT_DIR" ARCH="$(uname -m)" case "$ARCH" in x86_64) SB_ARCH="amd64" ;; aarch64) SB_ARCH="arm64" ;; *) err "不支持的架构: $ARCH" ;; esac SB_VERSION="1.11.0" SB_URL="https://github.com/SagerNet/sing-box/releases/download/v${SB_VERSION}/sing-box-${SB_VERSION}-linux-${SB_ARCH}.tar.gz" wait_for_apt log "更新系统包 ..." export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq curl wget nginx ufw ca-certificates python3 python3-venv python3-pip log "安装 sing-box ${SB_VERSION} ..." TMP="$(mktemp -d)" curl -fsSL "$SB_URL" | tar -xz -C "$TMP" --strip-components=1 install -m 755 "$TMP/sing-box" /usr/local/bin/sing-box rm -rf "$TMP" log "配置防火墙 ..." ufw --force reset ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp comment 'SSH' ufw allow 80/tcp comment 'HTTP-ACME-Panel' ufw allow 443/tcp comment 'Reality' ufw allow 8443:8499/udp comment 'Hysteria2-multi-node' ufw --force enable log "部署 Nginx fallback 站点 ..." mkdir -p /var/www/fallback cp "$ROOT_DIR/server/nginx/index.html" /var/www/fallback/ cp "$ROOT_DIR/server/nginx/fallback.conf" /etc/nginx/sites-available/fallback ln -sf /etc/nginx/sites-available/fallback /etc/nginx/sites-enabled/fallback rm -f /etc/nginx/sites-enabled/default log "部署 Nginx ACME + 管理面板反向代理 (80) ..." mkdir -p /var/www/acme sed -e "s|__DOMAIN__|${DOMAIN}|g" \ -e "s|__PANEL_LOCATION__|${PANEL_LOCATION}|g" \ -e "s|__PANEL_PREFIX__|${PANEL_PREFIX}|g" \ -e "s|__PANEL_ALLOW__|${PANEL_ALLOW_BLOCK}|g" \ "$ROOT_DIR/server/nginx/acme.conf.template" \ > /etc/nginx/sites-available/acme ln -sf /etc/nginx/sites-available/acme /etc/nginx/sites-enabled/acme nginx -t && systemctl enable nginx && systemctl restart nginx log "申请 TLS 证书 (Let's Encrypt) ..." mkdir -p /etc/sing-box/certs if [[ ! -f /root/.acme.sh/acme.sh ]]; then curl -fsSL https://get.acme.sh | sh -s email="$ACME_EMAIL" fi # shellcheck disable=SC1091 source /root/.acme.sh/acme.sh.env || true CURRENT_IP="$(curl -4 -fsSL ifconfig.me 2>/dev/null || curl -4 -fsSL ip.sb)" if [[ "$CURRENT_IP" != "$VPS_IP" ]]; then err "域名 $DOMAIN 需先解析到 VPS IP ($VPS_IP),当前 VPS 出口 IP 为 $CURRENT_IP" fi /root/.acme.sh/acme.sh --set-default-ca --server letsencrypt if [[ ! -f "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" ]]; then /root/.acme.sh/acme.sh --issue -d "$DOMAIN" -w /var/www/acme --force fi log "安装 TLS 证书到 sing-box ..." /root/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ --key-file /etc/sing-box/certs/privkey.pem \ --fullchain-file /etc/sing-box/certs/fullchain.pem rm -f /etc/nginx/sites-enabled/panel /etc/nginx/sites-available/panel nginx -t && systemctl reload nginx log "安装 Python 面板依赖 ..." python3 -m venv "$ROOT_DIR/panel/venv" "$ROOT_DIR/panel/venv/bin/pip" install -q --upgrade pip "$ROOT_DIR/panel/venv/bin/pip" install -q -r "$ROOT_DIR/panel/requirements.txt" "$ROOT_DIR/panel/venv/bin/python" -c "import flask" \ || err "面板依赖安装失败,请检查网络后重试" log "初始化节点数据库 ..." "$ROOT_DIR/panel/venv/bin/python" "$ROOT_DIR/panel/init_db.py" log "生成 sing-box 服务端配置 (Hysteria2) ..." python3 "$ROOT_DIR/scripts/render-server.py" log "安装 Xray (VLESS Reality 443) ..." bash -c "$(curl -fsSL https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install log "生成 Xray 服务端配置 ..." python3 "$ROOT_DIR/scripts/render-xray.py" log "创建 sing-box systemd 服务 ..." cat > /etc/systemd/system/sing-box.service <<'UNIT' [Unit] Description=sing-box service After=network-online.target nginx.service Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/sing-box run -c /etc/sing-box/config.json Restart=on-failure RestartSec=5 LimitNOFILE=1048576 [Install] WantedBy=multi-user.target UNIT log "创建管理面板 systemd 服务 ..." cat > /etc/systemd/system/jiedian-panel.service <