From 5d9061064923b336063b56e8bf0d00a53c1cab17 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 18 Jun 2026 07:46:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(single-node):=20=E6=94=B9=20SQLite=20+=20?= =?UTF-8?q?=E9=9D=9E=E7=89=B9=E6=9D=83=20pangolin=20=E7=94=A8=E6=88=B7=20+?= =?UTF-8?q?=20=E5=85=8D=20Docker(512MB=20=E5=8F=AF=E8=B7=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为小内存机收尾单机栈,已在 racknerd(107.172.55.251)实跑验证通过: - 数据库:MySQL(docker)→ SQLite 文件(DB_DRIVER=sqlite);Redis 改 apt 原生 (免 Docker);依赖 redis-server/sqlite3/polkitd/sing-box 由 deploy.sh 自动装 - 运行身份:三 systemd 单元 User=root → User=pangolin;sing-box 经 AmbientCapabilities 绑 443;新增 polkit 规则让 pangolin 仅可重启 sing-box - 二进制:支持预置(机上无 go 时用交叉编译产物;SQLite 纯 Go 免 CGO) - 端口:REALITY 默认 443/tcp(对齐 bootstrap 防火墙);deploy.sh 放行 8080/tcp - 修 sing-box.service:ExecStart 按实际安装路径渲染(官方 .deb 在 /usr/bin) - Hy2 默认关闭:控制面尚未下发 Hy2 TLS 证书(handler_grpc.go 仅发 ListenPort) → 开启会 missing certificate;待补全证书后传 HY2_PORT=443 开启 - 删 docker-compose.yml;seed TRUE→1;README 同步 Co-Authored-By: Claude Opus 4.8 --- deploy/single-node/README.md | 39 ++- deploy/single-node/deploy.sh | 260 +++++++++--------- deploy/single-node/docker-compose.yml | 34 --- .../polkit/49-pangolin-singbox.rules | 10 + deploy/single-node/seed-node.sql.tmpl | 2 +- .../systemd/pangolin-agent.service | 6 +- .../systemd/pangolin-server.service | 8 +- deploy/single-node/systemd/sing-box.service | 3 +- 8 files changed, 181 insertions(+), 181 deletions(-) delete mode 100644 deploy/single-node/docker-compose.yml create mode 100644 deploy/single-node/polkit/49-pangolin-singbox.rules diff --git a/deploy/single-node/README.md b/deploy/single-node/README.md index e40f3f7..80086a9 100644 --- a/deploy/single-node/README.md +++ b/deploy/single-node/README.md @@ -1,10 +1,11 @@ -# 单节点全套部署(档 3 真实连接) +# 单节点全套部署(真实连接,SQLite + 非特权用户) -一台全新 Ubuntu/Debian VPS 上跑 **整套** pangolin:MySQL + Redis + 控制面(HTTP -API + gRPC agent 服务)+ 节点 agent + sing-box 数据面。客户端连这台 VPS 的公网 -IP,真实出网(出口 IP = VPS IP)。 +一台全新 Debian 12 VPS 上跑 **整套** pangolin:**SQLite(文件)+ Redis(apt)** + 控制面 +(HTTP API + gRPC agent 服务)+ 节点 agent + sing-box 数据面,**全部以非特权 `pangolin` +用户运行,免 Docker**。客户端连这台 VPS 的公网 IP,真实出网(出口 IP = VPS IP)。 -> 不碰 EC2 生产、不用本机出网。这套是独立的端到端验证环境。 +> 为小内存机(512MB)调优:SQLite 取代 MySQL、apt 原生 Redis 取代 docker 容器。 +> 配合 `deploy/bootstrap/`(系统加固/防火墙/监控)使用。 ## 架构(一台机器内) @@ -19,23 +20,37 @@ IP,真实出网(出口 IP = VPS IP)。 │ │ connect → Hub.Push(Upsert dp_uuid) │ │ ▼ (gRPC Subscribe 命令流, mTLS) │ │ pangolin-agent ── 渲染 sing-box SERVER 配置 → systemctl restart │ -│ ▼ │ -│ sing-box VLESS+REALITY :11443 (+ Hysteria2 :443) → 直连出网 │ -│ MySQL :3306 (loopback) Redis :6379 (loopback) [docker] │ +│ ▼ (polkit 规则允许 pangolin 仅重启 sing-box) │ +│ sing-box VLESS+REALITY :443/tcp → 直连出网 │ +│ SQLite /var/lib/pangolin/pangolin.db Redis :6379 (apt, loopback) │ └─────────────────────────────────────────────────────────────────┘ +(三服务均 User=pangolin;sing-box 经 AmbientCapabilities 绑 443) ``` +> ⚠️ **Hy2 默认关闭**:控制面尚未接线 Hy2 的 TLS 证书下发(`handler_grpc.go` 只发 +> ListenPort,不发 CertPath/KeyPath),开启会导致 sing-box `missing certificate`。 +> 待补全证书下发后,传 `HY2_PORT=443` 开启。当前数据面=REALITY(443/tcp)。 + 凭证天然自洽:connect 给客户端的 REALITY `uuid = dp_uuid`、Hy2 `password = DeriveHy2Password(dp_uuid, NODE_DERIVE_KEY)`;agent 渲染 server 配置时用 **同一** `NODE_DERIVE_KEY` 和 dp_uuid,两端一致。 -## 部署(VPS 上一条命令) +## 部署 -前置:root、`docker`(含 compose v2)、`openssl`、`go`、`sing-box`(脚本会尝试自动装)。 +前置:root、`openssl`。`redis-server`/`sqlite3`/`polkitd`/`sing-box` 由 deploy.sh 自动装。 +二进制:机上有 `go` 则就地构建;**512MB 机器推荐别处交叉编译后预置**(无需机上装 Go): ```bash -# 1. 把仓库 checkout 到 VPS(git clone 或 scp) -# 2. 一键起全套(VPS_IP 不传则自动探测公网 IP) +# 在开发机交叉编译(SQLite 纯 Go,免 CGO),scp 到 VPS 的 /usr/local/bin/ +cd server && for c in server agent nodectl migrate; do + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/p-$c ./cmd/$c +done +scp /tmp/p-server :/usr/local/bin/pangolin-server +scp /tmp/p-agent :/usr/local/bin/pangolin-agent +scp /tmp/p-migrate :/usr/local/bin/pangolin-migrate +scp /tmp/p-nodectl :/usr/local/bin/nodectl + +# scp 本目录到 VPS 的 /deploy/single-node/,再一键起(VPS_IP 不传则自动探测) sudo VPS_IP=<你的公网IP> bash deploy/single-node/deploy.sh ``` diff --git a/deploy/single-node/deploy.sh b/deploy/single-node/deploy.sh index 20dce98..e420689 100755 --- a/deploy/single-node/deploy.sh +++ b/deploy/single-node/deploy.sh @@ -1,27 +1,17 @@ #!/usr/bin/env bash -# deploy.sh — one-command single-node deploy of the FULL pangolin stack on a -# fresh Ubuntu/Debian VPS: MySQL + Redis (docker) + control plane (HTTP+gRPC) + -# node agent + sing-box data plane, all on this one box. The client connects to -# this VPS's public IP and egresses through it. +# deploy.sh — 单机全栈部署,为小内存机(512MB)调优:SQLite(文件)+ Redis(apt) +# + 控制面 + agent + sing-box,全部以**非特权 pangolin 用户**运行。免 Docker。 # -# Idempotent: re-running refreshes binaries/config without rotating the REALITY -# keypair or datastore (so already-issued client configs keep working). +# 幂等:重跑只刷新二进制/配置,不轮换 REALITY 密钥或数据库(已发的客户端配置不失效)。 # -# Usage (as root, from the repo checkout on the VPS): -# sudo VPS_IP=203.0.113.10 bash deploy/single-node/deploy.sh +# 用法(root): +# sudo VPS_IP=1.2.3.4 bash deploy/single-node/deploy.sh # -# Required: -# VPS_IP public IPv4 the client dials (auto-detected if unset) -# Optional (sensible defaults): -# NODE_UUID fixed node uuid (default below) -# REALITY_SNI masquerade SNI (www.apple.com) -# REGION / NAME_ZH / NAME_EN -# HTTP_PORT GRPC_PORT REALITY_PORT HY2_PORT -# SMTP_HOST SMTP_PORT SMTP_USERNAME SMTP_PASSWORD SMTP_FROM (real email; else -# verification codes are printed to the server journal) +# 二进制:机上有 `go` 则就地构建;否则需**预先**把 pangolin-{server,agent,migrate}+nodectl +# 放进 /usr/local/bin(512MB 推荐:别处交叉编译 `CGO_ENABLED=0 GOOS=linux GOARCH=amd64`, +# 因 SQLite 走纯 Go 的 modernc,无需 CGO)。 set -euo pipefail -# ── 0. config & preflight ──────────────────────────────────────────────────── REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" HERE="$REPO_ROOT/deploy/single-node" @@ -32,104 +22,115 @@ NAME_ZH="${NAME_ZH:-单节点 · 测试}" NAME_EN="${NAME_EN:-Single Node}" HTTP_PORT="${HTTP_PORT:-8080}" GRPC_PORT="${GRPC_PORT:-9443}" -REALITY_PORT="${REALITY_PORT:-11443}" -HY2_PORT="${HY2_PORT:-443}" +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" -for c in openssl docker go; do - command -v "$c" >/dev/null 2>&1 || die "missing dependency: $c (install it first)." +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 -docker compose version >/dev/null 2>&1 || die "docker compose v2 plugin required." +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 not found — attempting official install ..." + warn "sing-box 未安装 — 尝试官方脚本安装 ..." curl -fsSL https://sing-box.app/install.sh | sh \ - || die "sing-box install failed; install it manually then re-run." + || 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 "could not auto-detect VPS_IP; pass VPS_IP=... explicitly." - log "auto-detected VPS_IP=$VPS_IP" + [ -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" -chmod 700 "$ETC" "$AGENT_ETC" +mkdir -p "$ETC" "$AGENT_ETC" "$SB_ETC" "$DATA_DIR" -# ── 1. build & install binaries ────────────────────────────────────────────── -log "building binaries (server, agent, nodectl, migrate) ..." -( - cd "$REPO_ROOT/server" - for cmd in server agent nodectl migrate; do - go build -o "$BIN/pangolin-tmp-$cmd" "./cmd/$cmd" +# ── 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 -) -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" - -# ── 2. datastores (MySQL + Redis via docker compose) ───────────────────────── -if [ ! -f "$ETC/db.env" ]; then - log "generating MySQL root password ..." - printf 'MYSQL_ROOT_PASSWORD=%s\n' "$(openssl rand -hex 24)" > "$ETC/db.env" - chmod 600 "$ETC/db.env" fi -# shellcheck disable=SC1091 -. "$ETC/db.env" -log "starting MySQL + Redis ..." -docker compose --env-file "$ETC/db.env" -f "$HERE/docker-compose.yml" up -d -log "waiting for MySQL ..." -until docker exec pangolin-mysql mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent >/dev/null 2>&1; do - sleep 1 -done -log "MySQL ready." +# ── 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." -# ── 3. control-plane secrets (idempotent) ──────────────────────────────────── +# ── 4. 控制面密钥(幂等)────────────────────────────────────────────────────── if [ ! -f "$ETC/jwt_private.pem" ]; then - log "generating RS256 JWT keypair ..." + log "生成 RS256 JWT 密钥对 ..." openssl genrsa -out "$ETC/jwt_private.pem" 2048 openssl rsa -in "$ETC/jwt_private.pem" -pubout -out "$ETC/jwt_public.pem" - chmod 600 "$ETC/jwt_private.pem" fi [ -f "$ETC/webhook_secret" ] || openssl rand -hex 32 > "$ETC/webhook_secret" [ -f "$ETC/derive_key" ] || openssl rand -hex 32 > "$ETC/derive_key" -chmod 600 "$ETC/webhook_secret" "$ETC/derive_key" WEBHOOK_SECRET="$(cat "$ETC/webhook_secret")" NODE_DERIVE_KEY="$(cat "$ETC/derive_key")" -# Node CA: generate here (ECDSA P-256, SEC1 "EC PRIVATE KEY" — the format -# mtls.NewCA loads). The control plane loads this existing CA on start; the agent -# pins it during enroll, and it signs the gRPC server cert below. +# Node CA(ECDSA P-256,SEC1 "EC PRIVATE KEY";mtls.NewCA 读它) if [ ! -f "$ETC/ca.key" ] || [ ! -f "$ETC/ca.crt" ]; then - log "generating Node CA ..." + 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" - chmod 600 "$ETC/ca.key" fi -# gRPC server cert: CA-signed, serverAuth EKU, SAN localhost+127.0.0.1 (the agent -# dials localhost:GRPC_PORT and validates the cert against the Node CA). +# gRPC server cert(CA 签发,SAN localhost+127.0.0.1) if [ ! -f "$ETC/grpc.key" ] || [ ! -f "$ETC/grpc.crt" ]; then - log "issuing gRPC server cert (signed by Node CA) ..." + 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" + openssl req -new -key "$ETC/grpc.key" -out "$ETC/grpc.csr" -subj "/CN=pangolin-control-plane" EXT="$(mktemp)" cat > "$EXT" < "$ETC/reality.env" - chmod 600 "$ETC/reality.env" fi # shellcheck disable=SC1091 . "$ETC/reality.env" -# ── 5. server.env ───────────────────────────────────────────────────────────── -log "writing server.env ..." +# ── 6. server.env(SQLite)───────────────────────────────────────────────────── +log "写 server.env(DB_DRIVER=sqlite)..." cat > "$ETC/server.env" <> "$ETC/server.env" < "$SEED" -docker exec -i pangolin-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" pangolin < "$SEED" +sqlite3 "$DB_FILE" < "$SEED" rm -f "$SEED" -# ── 7. install systemd units & start control plane ─────────────────────────── -log "installing systemd units ..." -install -m 644 "$HERE/systemd/pangolin-server.service" /etc/systemd/system/ -install -m 644 "$HERE/systemd/pangolin-agent.service" /etc/systemd/system/ -install -m 644 "$HERE/systemd/sing-box.service" /etc/systemd/system/ -systemctl daemon-reload - -log "starting control plane (pangolin-server) ..." -systemctl enable --now pangolin-server.service -# Wait for the gRPC server to accept the agent. -log "waiting for control plane 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 "control plane healthy." - -# ── 8. issue bootstrap token + wire agent ──────────────────────────────────── -log "issuing agent bootstrap token ..." +# ── 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 "failed to issue bootstrap token." - -# Pre-pin the CA so the agent's first enroll verifies the server (else it falls -# back to InsecureSkipVerify for that single call). +[ -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/ +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 -# ── 9. summary ──────────────────────────────────────────────────────────────── -log "done. Stack is up on this VPS." +# 放行控制面 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 + +# ── 11. 摘要 ────────────────────────────────────────────────────────────────── +log "完成。单机栈已起(SQLite + pangolin 用户)。" cat <