# 单节点全套部署（真实连接,SQLite + 非特权用户）

一台全新 Debian 12 VPS 上跑 **整套** pangolin：**SQLite（文件）+ Redis（apt）** + 控制面
（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、`openssl`。`redis-server`/`sqlite3`/`polkitd`/`sing-box` 由 deploy.sh 自动装。
二进制:机上有 `go` 则就地构建;**512MB 机器推荐别处交叉编译后预置**(无需机上装 Go):

```bash
# 在开发机交叉编译(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`。要真发：

```bash
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 上）

```bash
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.json` 并 `systemctl restart
sing-box`。在 connect 之前 server 配置里 reality-in 的 users 可能为空（没有已连用户），
客户端 connect 后 Hub 推 Upsert，agent 会把 `dp_uuid` 加进 users 并重启 sing-box。

## 客户端接线（本机，无需改代码）

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

1. **API 地址**
   ```bash
   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 免密 sudo（PoC 用，正式走 SMJobBless）
   ```
   # /etc/sudoers.d/pangolin-singbox
   <你的用户名> ALL=(root) NOPASSWD: /opt/homebrew/bin/sing-box
   ```
4. **真节点 uuid**：seed 的节点 uuid 非空，`connection_provider` 会自动走真实分支
   （`ConnectApi.fetchConfig` → `bridge.start`），无需改代码。

## 验证出口（端到端）

客户端连上后，在本机：

```bash
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。
