Files
pangolin/deploy/single-node
wangjia ea548ad7cf
ci-pangolin / Lint — shellcheck (push) Successful in 8s
ci-pangolin / OpenAPI Sync Check (push) Successful in 18s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Failing after 14s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 4s
ci-pangolin / Go — build + test (push) Successful in 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 13s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m3s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
fix(deploy): agent 不再随 server 停 —— 解耦 systemd 依赖(#8 静默掉线根因)
Requires=pangolin-server → Wants=(软依赖);Restart=on-failure → always +
StartLimitIntervalSec=0。根因:Requires= 使每次重启 server 都把 agent 一起拖停
且不自起 → 节点静默 down。agent 本就靠 gRPC 自重连,不该绑 server 生死。
线上 /etc/systemd 已同步 daemon-reload,并重启 server 验证 agent 全程存活+重连。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:22:20 +08:00
..

单节点全套部署(真实连接,SQLite + 非特权用户)

一台全新 Debian 12 VPS 上跑 整套 pangolinSQLite(文件)+ Redisapt + 控制面 HTTP API + gRPC agent 服务)+ 节点 agent + sing-box 数据面,全部以非特权 pangolin 用户运行,免 Docker。客户端连这台 VPS 的公网 IP,真实出网(出口 IP = VPS IP)。

为小内存机(512MB)调优:SQLite 取代 MySQL、apt 原生 Redis 取代 docker 容器。 配合 deploy/bootstrap/(系统加固/防火墙/监控)使用。

架构(一台机器内)

客户端 (mac/手机)
   │  ① POST /v1/auth/code → 注册/登录 (JWT)
   │  ② GET  /v1/nodes      → 看到本节点 (status=up)
   │  ③ POST /v1/nodes/{uuid}/connect → 控制面下发 sing-box CLIENT 配置 (REALITY)
   ▼
┌───────────────────────────── VPS ──────────────────────────────┐
│ pangolin-server  HTTP :8080  +  gRPC :9443                      │
│      │ connect → Hub.Push(Upsert dp_uuid)                       │
│      ▼ (gRPC Subscribe 命令流, mTLS)                            │
│ pangolin-agent  ── 渲染 sing-box SERVER 配置 → systemctl restart │
│      ▼ (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,两端一致。

部署

前置:root、opensslredis-server/sqlite3/polkitd/sing-box 由 deploy.sh 自动装。 二进制:机上有 go 则就地构建;512MB 机器推荐别处交叉编译后预置(无需机上装 Go):

# 在开发机交叉编译(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  <vps>:/usr/local/bin/pangolin-server
scp /tmp/p-agent   <vps>:/usr/local/bin/pangolin-agent
scp /tmp/p-migrate <vps>:/usr/local/bin/pangolin-migrate
scp /tmp/p-nodectl <vps>:/usr/local/bin/nodectl

# scp 本目录到 VPS 的 <repo>/deploy/single-node/,再一键起(VPS_IP 不传则自动探测)
sudo VPS_IP=<你的公网IP> bash deploy/single-node/deploy.sh

幂等:重跑只刷新二进制 + 配置,轮换 REALITY 密钥 / 数据库(已发的客户端配置不失效)。

真发邮件(可选)

不传 SMTP_* 时,注册验证码打到 journalctl -u pangolin-server。要真发:

sudo VPS_IP=<IP> \
  SMTP_HOST=smtp.resend.com SMTP_PORT=587 SMTP_USERNAME=resend \
  SMTP_PASSWORD=<resend-api-key> SMTP_FROM='穿山甲 <noreply@mail.51yanmei.com>' \
  bash deploy/single-node/deploy.sh

验证(VPS 上)

journalctl -u pangolin-agent -n 50 --no-pager     # enrolled successfully + Register
systemctl status sing-box --no-pager              # active (running)
grep -A3 reality-in /etc/sing-box/config.json     # listen_port 11443 + 你的 short_id
ss -tlnp | grep 11443                             # sing-box 在监听

agent 第一次 enroll 后会渲染 /etc/sing-box/config.jsonsystemctl restart sing-box。在 connect 之前 server 配置里 reality-in 的 users 可能为空(没有已连用户), 客户端 connect 后 Hub 推 Upsertagent 会把 dp_uuid 加进 users 并重启 sing-box。

客户端接线(本机,无需改代码)

客户端代码已就绪(connect 透传、桌面 sing-box 子进程 + sudo TUN)。只需指向本 VPS

  1. API 地址
    flutter run -d macos --dart-define=PANGOLIN_API_URL=http://<VPS_IP>:8080
    
  2. 本机 sing-box 二进制:装 brew install sing-box,并让 app 能找到它 (环境变量 PANGOLIN_SINGBOX_BIN=/opt/homebrew/bin/sing-box--dart-define 传不进进程 env,用真实环境变量或放到 kernel_process 查找路径)。
  3. macOS TUN 提权:给 sing-box 免密 sudoPoC 用,正式走 SMJobBless
    # /etc/sudoers.d/pangolin-singbox
    <你的用户名> ALL=(root) NOPASSWD: /opt/homebrew/bin/sing-box
    
  4. 真节点 uuidseed 的节点 uuid 非空,connection_provider 会自动走真实分支 ConnectApi.fetchConfigbridge.start),无需改代码。

验证出口(端到端)

客户端连上后,在本机:

curl https://api.ipify.org      # 出口 IP 应等于 <VPS_IP>

出口 IP == VPS IP 即真实远端代理打通。

文件 / 路径

路径 内容
/etc/pangolin/server.env 控制面环境变量(DB/JWT/CA/gRPC/derive key
/etc/pangolin/{ca,grpc}.{key,crt} Node CA + gRPC server 证书(CA 签发,SAN localhost
/etc/pangolin/reality.env REALITY 密钥对 + short_id(跨重部署稳定)
/etc/pangolin/db.env MySQL root 密码
/etc/pangolin-agent/agent.env agent 环境变量(含一次性 bootstrap token
/etc/pangolin-agent/{node.key,node.crt,ca.crt} agent enroll 后持久化的 mTLS 身份
/etc/sing-box/config.json agent 渲染的 sing-box SERVER 配置
/usr/local/bin/{pangolin-server,pangolin-agent,nodectl,pangolin-migrate} 二进制

排查

  • agent 起不来 / enroll 失败journalctl -u pangolin-agent。检查 pangolin-server 是否已起 gRPC(日志应有 gRPC,不是 "gRPC not configured")—— 需要 server.env 里 GRPC_ADDR + CA_* + GRPC_* 全配(deploy.sh 已配好)。
  • sing-box 反复重启:首次 enroll 前 /etc/sing-box/config.json 不存在,属正常; enroll 后应自愈。仍失败看 journalctl -u sing-box,多半是 REALITY 配置/端口占用。
  • 客户端 connect 返回 4xx:确认 JWT 有效(先登录)、节点 status='up'
  • 出口 IP 不是 VPS:检查客户端是否真的起了 TUN(sudo 提权)、route final=auto。