Files
pangolin/design/server/ARCHITECTURE.md
T
wangjia ab2bbaf683 tsk_9uMrd9kUpmVA: 数据面定稿 + 修订 ARCHITECTURE.md connect 契约
决策:libbox 统一承载 REALITY/Hy2 outbound(方案 C),弃用 WireGuard peer 注册。
现网 deploy/ 基础设施(singbox Hy2 + xray REALITY)已验证,零改动复用。

变更文件:
- design/server/ARCHITECTURE.md v0.2:
  · §0 数据面描述改为 sing-box libbox (REALITY/Hy2)
  · §1 拓扑图更新(wireguard+agent → singbox+agent,客户端标注 libbox 内嵌)
  · §2 devices 表移除 pubkey;nodes 表改为 reality_public_key/short_id/uuid/hy2_password
  · §3 connect 响应由 WireGuard 配置改为完整 sing-box config JSON
  · §3.1(新增)connect 契约详细规范:四块 inbounds/outbounds/route/dns、
    客户端透传原则、字段与 deploy/ 模板逐字段对照、占位符替换说明
  · §4.3 连接流程更新(peer 注册 → config JSON 渲染下发)
  · 附录 A(新增)v0.1→v0.2 变更汇总
- doc/plans/11-libbox-bridge.md(新建):
  完整决策记录(背景/备选/结论/影响面),任务链 11A→11C→11D/E/F→11H
- doc/plans/client-connect-config.example.json(新建):
  最小示例 config,字段与 deploy/ 模板对齐,可用 sing-box check -c 校验

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 01:43:17 +08:00

225 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 穿山甲 · 后端架构设计(v0.2 — 数据面改为 libbox/sing-box)
> 本文档是给 Claude Code 的实现蓝本:先审阅 → 修订 → 再按模块实现。
> 产品语境见 `../CLAUDE.md`(设计系统)。脱敏规则同样适用于日志、报错文案与对外文档。
---
## 0. 设计原则
1. **控制面 / 数据面彻底分离**:API 服务器只管账户、套餐、节点目录、兑换;用户流量只走加速节点(sing-box libboxREALITY/Hy2 outbound),绝不经过 API。
2. **App 内无支付**:资金流全部外部化(发卡店/TG/LINE/邮箱)→ 后端只做「激活码」的生成、分发对账与兑换。
3. **节点可秒级灰度**:节点列表带版本,被封节点能即时下架;客户端缓存最后一份可用列表兜底。
4. **最小数据原则(无日志承诺)**:不记录浏览内容/目的地;只保留计费与风控所需的最小元数据(用量字节数、设备数、最后活跃时间),并在文档中公开口径。
---
## 1. 系统拓扑
```
┌────────────────┐ HTTPS ┌─────────────────┐
│ Flutter 客户端 │ ───────▶ │ API (控制面) │── Postgres / Redis
│ (libbox 内嵌) │ │ Go + chi/gin │
└───────┬────────┘ └───────┬─────────┘
│ REALITY / Hy2 │ 节点注册/心跳 (mTLS gRPC)
│ (libbox outbound) ┌────────▼─────────┐ ┌────────────────┐
└──────────────────▶│ 加速节点 (数据面) │ │ 发卡店 Webhook │
│ singbox + agent │ │ (激活码入库) │
└──────────────────┘ └────────────────┘
```
- **API**:Go(推荐,chi/gin 生态成熟)。单体起步,按模块分包,未来可拆。
- **节点 agent**:每台加速节点跑一个轻量 agent:注册、心跳、上报负载、向控制面同步节点凭证(REALITY 公钥/Hy2 口令)。与 API 间用 mTLS gRPC。
- **存储**:Postgres(主数据)+ Redis(验证码、限流、节点实时负载)。
---
## 2. 数据模型(核心表)
```sql
users (id, email UNIQUE, pw_hash, created_at, status)
devices (id, user_id FK, name, platform, last_seen, created_at)
plans (id, code['free'|'pro'|'team'], max_devices, max_routes,
daily_minutes NULL=, ad_gate bool) -- free: 1 节点/10 分钟/须看广告
subscriptions (id, user_id FK, plan_id FK, expires_at, source['trial'|'code'], created_at)
codes (id, code UNIQUE, plan_id, duration_days, batch_id, channel,
status['unused'|'redeemed'|'void'], redeemed_by NULL, redeemed_at NULL)
code_batches (id, channel['store'|'tg'|'line'|'manual'], created_by, note, created_at)
nodes (id, region, name_zh, name_en, endpoint, protocol['reality'|'hy2'|'both'],
reality_public_key, reality_short_id, reality_uuid, hy2_password,
tags[], tier['free'|'pro'], status['up'|'down'|'draining'], weight)
-- 凭证字段由节点 agent 注册时上报,控制面渲染 connect 响应时使用
usage_daily (user_id, date, bytes_up, bytes_down, minutes_used, ad_unlocked_at NULL)
-- 仅字节数/分钟数,无目的地;ad_unlocked_at = 当日激励视频解锁时刻(免费版)
audit_log (id, actor, action, target, meta jsonb, at) -- 兑换/封禁/节点操作必记
```
要点:
- 试用 = 注册时自动插入 `subscriptions(plan=pro, expires_at=now()+7d, source='trial')`,一邮箱仅一次(套餐口径见 `../CLAUDE.md` §7)。
- 体验期后回落 free:仅 1 个基础节点(nodes.tier='free')、每日 10 分钟;每日首次连接前需激励视频解锁(客户端上报 → `POST /v1/ads/unlock``ad_unlocked_at`,connect 接口校验)。
- `codes.code` 用 Crockford Base32,16 位,带校验位;明文只出现一次(生成响应/发卡店),库里可存 hash。
---
## 3. API 契约(v1 摘要,正式版用 OpenAPI 定义)
```
POST /v1/auth/code {email} → 发 6 位验证码(Redis, 10min, 限 1/min)
POST /v1/auth/register {email, code, password} → 创建账户 + 7 天试用 + JWT
POST /v1/auth/login {email, password} → JWT (access 15min + refresh 30d)
POST /v1/auth/refresh {refresh_token}
GET /v1/me → 账户 + 订阅 + 用量摘要
GET /v1/me/devices → 设备列表
DELETE /v1/me/devices/:id → 移除设备(强制下线,下次 connect 校验时拒绝)
POST /v1/redeem {code} → 兑换激活码(幂等,审计)
POST /v1/ads/unlock {device_id, ad_token} → 免费版激励视频解锁当日时长(验广告 SDK 回执)
GET /v1/plans → 套餐目录(给客户端展示)
GET /v1/nodes ?if_version=N → 节点目录(按套餐过滤, 304 支持)
POST /v1/nodes/:id/connect {device_id} → 下发完整 sing-box config JSON(见 §3.1
POST /v1/nodes/:id/disconnect
GET /v1/usage ?days=7 → 用量曲线(统计页)
```
约定:错误体 `{code, message_zh, message_en}`;全部接口 JWT(除 auth);限流用 Redis 滑窗。
### 3.1 connect 契约 —— sing-box config JSON(数据面,v0.2 定稿)
> **输入来源**#5/#6(控制面 connect 接口实现)以本节为准;11H(M6 联调)强依赖本契约。
> **决策依据**:见 `doc/plans/11-libbox-bridge.md`(数据面协议选型记录)。
**请求体**
```json
{ "device_id": "<设备 ID>" }
```
**响应体** —— 完整 sing-box config JSON,包含以下四块:
| 块 | 职责 |
|----|------|
| `inbounds` | TUN 接管设备全流量;`strict_route: true` 实现 Kill-switchVPN 断线时阻断非隧道流量) |
| `outbounds` | REALITY outboundTCP,抗主动探测)+ Hy2 outboundUDP,高吞吐)+ `type:urltest` 自动测速选优组 + `block` |
| `route` | 私有 IP 直连;其余默认走 urltest 组(即走代理);`auto_detect_interface: true` |
| `dns` | 全部 DNS 走远端解析(防泄露);本地 DNS 仅用于 CN 直连域名(可选) |
**客户端职责(不可越界)**
```
收到 config JSON → libbox.start(configJson) // 原样透传,Dart 层禁止拼装/修改 config
```
Dart 层不组装 outbound,不修改任何字段,不缓存凭证——全部服务端生成、客户端透传。
**字段与 deploy/ 模板对照**
| 字段 | 客户端 config | deploy/ 对应 |
|------|-------------|-------------|
| VLESS `server_port` | `11443` | `xray/config.tmpl.json` `inbounds[0].port` |
| VLESS `uuid` | 服务端动态生成 | `xray/config.tmpl.json` `clients[0].id``__UUID__`|
| VLESS `flow` | `"xtls-rprx-vision"` | `xray/config.tmpl.json` `clients[0].flow` |
| REALITY `server_name` | `"www.apple.com"` | `xray/config.tmpl.json` `serverNames[0]` |
| REALITY `public_key` | 服务端动态生成 | 由 `__REALITY_PRIVATE_KEY__` 派生公钥 |
| REALITY `short_id` | 服务端动态生成 | `xray/config.tmpl.json` `shortIds[0]``__REALITY_SHORT_ID__`|
| Hy2 `server_port` | `443` | `singbox/config.tmpl.json` `inbounds[0].listen_port` |
| Hy2 `password` | 服务端动态生成 | `singbox/config.tmpl.json` `users[0].password``__HY2_PASSWORD__`|
| Hy2 `masquerade` | TLS `alpn: ["h3"]` | `singbox/config.tmpl.json` `tls.alpn` |
| TUN `strict_route` | `true`Kill-switch| — |
**最小示例 config**(可直连 deploy/ 测试节点,占位符须替换)
完整 JSON 见 `doc/plans/client-connect-config.example.json`。占位符替换说明:
| 占位符 | 实际值来源 |
|--------|-----------|
| `"ffffffff-ffff-ffff-ffff-ffffffffffff"` | xray `__UUID__`VLESS 用户 UUID|
| `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"` | `__REALITY_PRIVATE_KEY__` 对应的 base64url 公钥 |
| `"deadbeef"` | xray `__REALITY_SHORT_ID__`(十六进制,18 字节)|
| `"__HY2_PASSWORD__"` | singbox `__HY2_PASSWORD__`Hysteria2 用户口令)|
| `"__NODE_HOST__"` | 节点公网 IP 或域名 |
> 校验命令:`sing-box check -c doc/plans/client-connect-config.example.json`
---
## 4. 关键流程
### 4.1 注册(对齐客户端 UI:邮箱 → 验证码 → 设密码)
1. `POST /auth/code`:风控(IP+邮箱限频、一次性邮箱域黑名单)→ 发码。
2. `POST /auth/register`:验码 → 建号 → **自动开 7 天 PRO 试用** → 返回 JWT。
### 4.2 激活码生命周期
```
发卡店售出 → webhook(带签名) → codes 入库(status=unused, channel=store)
人工渠道(TG/LINE/邮箱) → 管理端批量生成 batch → 导出给客服
客户端 POST /redeem → 校验+事务:code→redeemed, 订阅顺延(叠加而非覆盖) → audit_log
```
- 兑换幂等:同一用户重复提交同一码返回首次结果。
- 风控:单用户兑换失败 5 次/小时锁 1 小时。
### 4.3 连接(数据面)
1. 客户端拉 `/nodes`(带套餐过滤 + 客户端测延迟自选)。
2. `POST /nodes/:id/connect {device_id}`
- API 校验订阅有效性与设备数限制(免费版另校验当日 `ad_unlocked_at` 与剩余分钟)。
-`nodes` 表取节点凭证(`reality_public_key`/`reality_short_id`/`reality_uuid`/`hy2_password`)。
- 服务端渲染完整 sing-box config JSON(见 §3.1),返回给客户端。
- 免费版:在 config JSON 的 `outbounds[urltest].outbounds` 中仅保留 `free` tier 节点;TTL 通过客户端侧 token 过期管理,服务端不主动踢连接。
3. 客户端收到 JSON → `libbox.start(configJson)`,原样透传,不修改。
4. 断开:`POST /nodes/:id/disconnect` → 服务端记录设备已断开(用于流量统计),客户端调 `libbox.stop()`
5. 节点 agent 每 30s 上报负载(在线连接数、带宽) → Redis,用于目录排序与"信号条"。
### 4.4 节点目录灰度
- `nodes` 任何变更 bump 全局 version;客户端带 `if_version` 轮询(或推送)。
- 节点被封:status=down 立即从目录消失;`draining` 用于计划下线(不接新连接)。
---
## 5. 安全与合规
- 密码 argon2id;JWT RS256,密钥轮换;全站 TLS1.3。
- 验证码与兑换接口必须有人机/限流防刷;注册防一次性邮箱。
- **无日志口径**(写进隐私政策并据实执行):不记录目的地址/DNS 查询/流量内容;仅 `usage_daily` 字节数。
- 管理端(内部)单独服务 + IP 白名单 + 双因素;一切敏感操作进 audit_log。
- 节点服务器上不落用户身份明文,peer 只对应 device_id。
---
## 6. 部署与运维(起步规模)
- API:单区域 2 实例 + LB;Postgres 托管(每日备份);Redis 托管。
- 节点:按地区购 VPSHK/JP/SG/US…),Ansible/脚本一键装 sing-box + agent;现网已有 `deploy/` 模板可复用;出口 IP 池可轮换。
- 监控:节点心跳缺失告警、兑换失败率、注册转化漏斗。
- CIGitHub Actions——lint + 单测 + OpenAPI 校验 + docker 镜像。
---
## 7. 给 Claude Code 的实现顺序
1. `openapi.yaml`(把 §3/§3.1 展开成正式契约)+ 数据库 migration
2. **auth 模块**(验证码/注册/登录/JWT)+ 测试
3. **codes 模块**(批量生成/webhook/兑换)+ 审计
4. devices + 订阅校验中间件
5. nodes 目录 + agent gRPC 协议(proto 文件)+ connect/disconnect(响应体按 §3.1 生成 sing-box config JSON
6. usage 统计 + 管理端最小后台
7. Flutter 端接 API(替换 UI Kit 演示数据;UI 规范见 design/libbox 桥接见 `doc/plans/11-libbox-bridge.md`
> 每个模块完成的定义:有单测、有 OpenAPI 同步、错误文案双语且符合脱敏口径。
---
## 附录 A:数据面协议变更说明(v0.1 → v0.2)
v0.1 数据面为 WireGuardpeer 注册模式)。v0.2 改为 **sing-box libboxREALITY/Hy2 outbound**
变更原因及完整决策记录:`doc/plans/11-libbox-bridge.md`(任务 tsk_9uMrd9kUpmVA)。
主要变化汇总:
| 项目 | v0.1 | v0.2 |
|------|------|------|
| 数据面协议 | WireGuard UDP peer | REALITYTCP+ Hy2UDP),libbox 统一 |
| connect 响应 | WireGuard 配置(endpoint/pubkey/IP/DNS| 完整 sing-box config JSON |
| 客户端行为 | 构建 WireGuard 隧道 | `libbox.start(configJson)` 原样透传 |
| 节点凭证 | WireGuard pubkey | REALITY public_key/short_id/uuid + Hy2 password |
| `devices` 表 | 含 `pubkey`WG| 移除 `pubkey`,不存储客户端密钥 |
| `nodes` 表 | 含 `pubkey`WG| 改为 `reality_public_key`/`reality_short_id`/`reality_uuid`/`hy2_password` |