ab2bbaf683
决策: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>
13 KiB
13 KiB
穿山甲 · 后端架构设计(v0.2 — 数据面改为 libbox/sing-box)
本文档是给 Claude Code 的实现蓝本:先审阅 → 修订 → 再按模块实现。 产品语境见
../CLAUDE.md(设计系统)。脱敏规则同样适用于日志、报错文案与对外文档。
0. 设计原则
- 控制面 / 数据面彻底分离:API 服务器只管账户、套餐、节点目录、兑换;用户流量只走加速节点(sing-box libbox,REALITY/Hy2 outbound),绝不经过 API。
- App 内无支付:资金流全部外部化(发卡店/TG/LINE/邮箱)→ 后端只做「激活码」的生成、分发对账与兑换。
- 节点可秒级灰度:节点列表带版本,被封节点能即时下架;客户端缓存最后一份可用列表兜底。
- 最小数据原则(无日志承诺):不记录浏览内容/目的地;只保留计费与风控所需的最小元数据(用量字节数、设备数、最后活跃时间),并在文档中公开口径。
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. 数据模型(核心表)
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(数据面协议选型记录)。
请求体
{ "device_id": "<设备 ID>" }
响应体 —— 完整 sing-box config JSON,包含以下四块:
| 块 | 职责 |
|---|---|
inbounds |
TUN 接管设备全流量;strict_route: true 实现 Kill-switch(VPN 断线时阻断非隧道流量) |
outbounds |
REALITY outbound(TCP,抗主动探测)+ Hy2 outbound(UDP,高吞吐)+ 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__(十六进制,1–8 字节) |
"__HY2_PASSWORD__" |
singbox __HY2_PASSWORD__(Hysteria2 用户口令) |
"__NODE_HOST__" |
节点公网 IP 或域名 |
校验命令:
sing-box check -c doc/plans/client-connect-config.example.json
4. 关键流程
4.1 注册(对齐客户端 UI:邮箱 → 验证码 → 设密码)
POST /auth/code:风控(IP+邮箱限频、一次性邮箱域黑名单)→ 发码。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 连接(数据面)
- 客户端拉
/nodes(带套餐过滤 + 客户端测延迟自选)。 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中仅保留freetier 节点;TTL 通过客户端侧 token 过期管理,服务端不主动踢连接。
- API 校验订阅有效性与设备数限制(免费版另校验当日
- 客户端收到 JSON →
libbox.start(configJson),原样透传,不修改。 - 断开:
POST /nodes/:id/disconnect→ 服务端记录设备已断开(用于流量统计),客户端调libbox.stop()。 - 节点 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 托管。
- 节点:按地区购 VPS(HK/JP/SG/US…),Ansible/脚本一键装 sing-box + agent;现网已有
deploy/模板可复用;出口 IP 池可轮换。 - 监控:节点心跳缺失告警、兑换失败率、注册转化漏斗。
- CI:GitHub Actions——lint + 单测 + OpenAPI 校验 + docker 镜像。
7. 给 Claude Code 的实现顺序
openapi.yaml(把 §3/§3.1 展开成正式契约)+ 数据库 migration- auth 模块(验证码/注册/登录/JWT)+ 测试
- codes 模块(批量生成/webhook/兑换)+ 审计
- devices + 订阅校验中间件
- nodes 目录 + agent gRPC 协议(proto 文件)+ connect/disconnect(响应体按 §3.1 生成 sing-box config JSON)
- usage 统计 + 管理端最小后台
- Flutter 端接 API(替换 UI Kit 演示数据;UI 规范见 design/;libbox 桥接见
doc/plans/11-libbox-bridge.md)
每个模块完成的定义:有单测、有 OpenAPI 同步、错误文案双语且符合脱敏口径。
附录 A:数据面协议变更说明(v0.1 → v0.2)
v0.1 数据面为 WireGuard(peer 注册模式)。v0.2 改为 sing-box libbox(REALITY/Hy2 outbound)。
变更原因及完整决策记录:doc/plans/11-libbox-bridge.md(任务 tsk_9uMrd9kUpmVA)。
主要变化汇总:
| 项目 | v0.1 | v0.2 |
|---|---|---|
| 数据面协议 | WireGuard UDP peer | REALITY(TCP)+ Hy2(UDP),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 |