#!/usr/bin/env bash # deploy.sh — 单机全栈部署,为小内存机(512MB)调优:SQLite(文件)+ Redis(apt) # + 控制面 + agent + sing-box,全部以**非特权 pangolin 用户**运行。免 Docker。 # # 幂等:重跑只刷新二进制/配置,不轮换 REALITY 密钥或数据库(已发的客户端配置不失效)。 # # 用法(root): # sudo VPS_IP=1.2.3.4 bash deploy/single-node/deploy.sh # # 二进制:机上有 `go` 则就地构建;否则需**预先**把 pangolin-{server,agent,migrate}+nodectl # 放进 /usr/local/bin(512MB 推荐:别处交叉编译 `CGO_ENABLED=0 GOOS=linux GOARCH=amd64`, # 因 SQLite 走纯 Go 的 modernc,无需 CGO)。 set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" HERE="$REPO_ROOT/deploy/single-node" NODE_UUID="${NODE_UUID:-22222222-2222-2222-2222-222222222222}" REALITY_SNI="${REALITY_SNI:-www.apple.com}" REGION="${REGION:-HK}" NAME_ZH="${NAME_ZH:-单节点 · 测试}" NAME_EN="${NAME_EN:-Single Node}" HTTP_PORT="${HTTP_PORT:-8080}" GRPC_PORT="${GRPC_PORT:-9443}" REALITY_PORT="${REALITY_PORT:-443}" # 与 bootstrap 防火墙放行的 443/tcp 对齐 # Hy2 默认【关闭】:控制面尚未接线 Hy2 TLS 证书(handler_grpc.go 仅发 ListenPort, # 不发 CertPath/KeyPath)→ 开启会令 sing-box 报 "missing certificate"。待代码补全 # Hy2 证书下发后再传 HY2_PORT=443 开启。 HY2_PORT="${HY2_PORT:-}" ETC="/etc/pangolin" AGENT_ETC="/etc/pangolin-agent" SB_ETC="/etc/sing-box" DATA_DIR="/var/lib/pangolin" DB_FILE="$DATA_DIR/pangolin.db" BIN="/usr/local/bin" PUSER="pangolin" log() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || die "must run as root (sudo)." command -v openssl >/dev/null 2>&1 || die "missing dependency: openssl" export DEBIAN_FRONTEND=noninteractive # ── 0. pangolin 非特权用户 ──────────────────────────────────────────────────── if ! id -u "$PUSER" >/dev/null 2>&1; then log "创建非特权用户 $PUSER" useradd --system --create-home --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$PUSER" fi # ── 1. 系统依赖(apt:Redis + sqlite3;免 Docker)─────────────────────────────── miss="" # polkitd:让非特权 pangolin 经 polkit 规则重启 sing-box(精简 Debian 默认无)。 for p in redis-server sqlite3 curl ca-certificates polkitd; do dpkg -s "$p" >/dev/null 2>&1 || miss="$miss $p" done if [ -n "$miss" ]; then log "安装依赖:$miss" apt-get update -q # shellcheck disable=SC2086 apt-get install -y --no-install-recommends $miss fi if ! command -v sing-box >/dev/null 2>&1; then warn "sing-box 未安装 — 尝试官方脚本安装 ..." curl -fsSL https://sing-box.app/install.sh | sh \ || die "sing-box 安装失败;请手动装后重跑。" fi if [ -z "${VPS_IP:-}" ]; then VPS_IP="$(curl -fsS --max-time 5 https://api.ipify.org || true)" [ -n "$VPS_IP" ] || die "无法自动探测 VPS_IP;请显式传 VPS_IP=..." log "自动探测 VPS_IP=$VPS_IP" fi ENDPOINT="$VPS_IP:$REALITY_PORT" mkdir -p "$ETC" "$AGENT_ETC" "$SB_ETC" "$DATA_DIR" # ── 2. 二进制(有 go 则构建;否则用预置)────────────────────────────────────── if command -v go >/dev/null 2>&1; then log "就地构建二进制(server/agent/nodectl/migrate)..." ( cd "$REPO_ROOT/server" for cmd in server agent nodectl migrate; do CGO_ENABLED=0 go build -o "$BIN/pangolin-tmp-$cmd" "./cmd/$cmd" done ) mv -f "$BIN/pangolin-tmp-server" "$BIN/pangolin-server" mv -f "$BIN/pangolin-tmp-agent" "$BIN/pangolin-agent" mv -f "$BIN/pangolin-tmp-nodectl" "$BIN/nodectl" mv -f "$BIN/pangolin-tmp-migrate" "$BIN/pangolin-migrate" else log "机上无 go → 使用预置二进制" for b in pangolin-server pangolin-agent nodectl pangolin-migrate; do [ -x "$BIN/$b" ] || die "缺二进制 $BIN/$b:机上无 go,请先交叉编译并放入 $BIN" done fi # ── 3. Redis(apt 原生 systemd 服务,绑 127.0.0.1)────────────────────────────── log "启动 Redis ..." systemctl enable --now redis-server >/dev/null 2>&1 || systemctl enable --now redis >/dev/null 2>&1 || true until redis-cli ping >/dev/null 2>&1; do sleep 1; done log "Redis ready." # ── 4. 控制面密钥(幂等)────────────────────────────────────────────────────── if [ ! -f "$ETC/jwt_private.pem" ]; then log "生成 RS256 JWT 密钥对 ..." openssl genrsa -out "$ETC/jwt_private.pem" 2048 openssl rsa -in "$ETC/jwt_private.pem" -pubout -out "$ETC/jwt_public.pem" fi [ -f "$ETC/webhook_secret" ] || openssl rand -hex 32 > "$ETC/webhook_secret" [ -f "$ETC/derive_key" ] || openssl rand -hex 32 > "$ETC/derive_key" WEBHOOK_SECRET="$(cat "$ETC/webhook_secret")" NODE_DERIVE_KEY="$(cat "$ETC/derive_key")" # Node CA(ECDSA P-256,SEC1 "EC PRIVATE KEY";mtls.NewCA 读它) if [ ! -f "$ETC/ca.key" ] || [ ! -f "$ETC/ca.crt" ]; then log "生成 Node CA ..." openssl ecparam -name prime256v1 -genkey -noout -out "$ETC/ca.key" openssl req -x509 -new -key "$ETC/ca.key" -days 3650 -out "$ETC/ca.crt" \ -subj "/O=Pangolin/CN=Pangolin Node CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" fi # gRPC server cert(CA 签发,SAN localhost+127.0.0.1) if [ ! -f "$ETC/grpc.key" ] || [ ! -f "$ETC/grpc.crt" ]; then log "签发 gRPC server 证书 ..." openssl ecparam -name prime256v1 -genkey -noout -out "$ETC/grpc.key" openssl req -new -key "$ETC/grpc.key" -out "$ETC/grpc.csr" -subj "/CN=pangolin-control-plane" EXT="$(mktemp)" cat > "$EXT" < "$ETC/reality.env" fi # shellcheck disable=SC1091 . "$ETC/reality.env" # ── 6. server.env(SQLite)───────────────────────────────────────────────────── log "写 server.env(DB_DRIVER=sqlite)..." cat > "$ETC/server.env" <> "$ETC/server.env" < "$SEED" sqlite3 "$DB_FILE" < "$SEED" rm -f "$SEED" # ── 8. 发 bootstrap token + 写 agent.env ────────────────────────────────────── log "签发 agent bootstrap token ..." TOKEN="$(REDIS_ADDR=127.0.0.1:6379 "$BIN/nodectl" bootstrap-token -node="$NODE_UUID")" [ -n "$TOKEN" ] || die "签发 bootstrap token 失败。" cp -f "$ETC/ca.crt" "$AGENT_ETC/ca.crt" cat > "$AGENT_ETC/agent.env" < /etc/systemd/system/sing-box.service chmod 644 /etc/systemd/system/sing-box.service install -d -m 755 /etc/polkit-1/rules.d install -m 644 "$HERE/polkit/49-pangolin-singbox.rules" /etc/polkit-1/rules.d/ # ── cloudflared(控制面 API 出站隧道,Cloudflare apt 源)───────────────────────── if ! command -v cloudflared >/dev/null 2>&1; then log "安装 cloudflared(Cloudflare apt 源)..." install -m 0755 -d /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ -o /usr/share/keyrings/cloudflare-main.gpg echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' \ > /etc/apt/sources.list.d/cloudflared.list apt-get update -qq apt-get install -y -qq cloudflared fi # apt 固定装到 /usr/bin;软链到 unit 期望的 /usr/local/bin(与其他 pangolin 二进制一致)。 CFD_BIN=/usr/bin/cloudflared [ -x "$CFD_BIN" ] || die "cloudflared 安装失败:$CFD_BIN 不存在。" ln -sf "$CFD_BIN" /usr/local/bin/cloudflared install -m 644 "$HERE/systemd/cloudflared.service" /etc/systemd/system/ systemctl daemon-reload log "启动控制面(pangolin-server,以 $PUSER 运行)..." systemctl enable --now pangolin-server.service log "等待控制面 HTTP :$HTTP_PORT ..." until curl -fsS --max-time 2 "http://127.0.0.1:$HTTP_PORT/healthz" >/dev/null 2>&1; do sleep 1; done log "控制面健康。" log "启用 sing-box + 启动 agent ..." systemctl enable sing-box.service systemctl enable --now pangolin-agent.service # 放行控制面 API 端口(若 ufw 启用)。⚠️ 明文,生产应前置 TLS。 if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow "${HTTP_PORT}/tcp" >/dev/null 2>&1 || true log "ufw 放行 ${HTTP_PORT}/tcp(控制面 API,明文)" fi # ── cloudflared:按需启用(需 /etc/pangolin/cloudflared.env 提供 TUNNEL_TOKEN)─── if [ -f /etc/pangolin/cloudflared.env ]; then log "启用 cloudflared(检测到 /etc/pangolin/cloudflared.env)..." systemctl enable --now cloudflared.service else log "cloudflared 已安装但未启动:等 /etc/pangolin/cloudflared.env(TUNNEL_TOKEN)就绪后执行:" log " systemctl enable --now cloudflared.service" fi # ── 11. 摘要 ────────────────────────────────────────────────────────────────── log "完成。单机栈已起(SQLite + pangolin 用户)。" cat <