From ab2bbaf68359893061637979f8ce062eecd10b58 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 13 Jun 2026 01:43:17 +0800 Subject: [PATCH] =?UTF-8?q?tsk=5F9uMrd9kUpmVA:=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=9D=A2=E5=AE=9A=E7=A8=BF=20+=20=E4=BF=AE=E8=AE=A2=20ARCHITEC?= =?UTF-8?q?TURE.md=20connect=20=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 决策: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 --- design/server/ARCHITECTURE.md | 147 +++++++++++++++---- doc/plans/11-libbox-bridge.md | 64 ++++++++ doc/plans/client-connect-config.example.json | 99 +++++++++++++ 3 files changed, 278 insertions(+), 32 deletions(-) create mode 100644 doc/plans/11-libbox-bridge.md create mode 100644 doc/plans/client-connect-config.example.json diff --git a/design/server/ARCHITECTURE.md b/design/server/ARCHITECTURE.md index 6da9312..3c201a2 100644 --- a/design/server/ARCHITECTURE.md +++ b/design/server/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# 穿山甲 · 后端架构设计(初稿 v0.1) +# 穿山甲 · 后端架构设计(v0.2 — 数据面改为 libbox/sing-box) > 本文档是给 Claude Code 的实现蓝本:先审阅 → 修订 → 再按模块实现。 > 产品语境见 `../CLAUDE.md`(设计系统)。脱敏规则同样适用于日志、报错文案与对外文档。 @@ -6,7 +6,7 @@ --- ## 0. 设计原则 -1. **控制面 / 数据面彻底分离**:API 服务器只管账户、套餐、节点目录、兑换;用户流量只走加速节点(WireGuard),绝不经过 API。 +1. **控制面 / 数据面彻底分离**:API 服务器只管账户、套餐、节点目录、兑换;用户流量只走加速节点(sing-box libbox,REALITY/Hy2 outbound),绝不经过 API。 2. **App 内无支付**:资金流全部外部化(发卡店/TG/LINE/邮箱)→ 后端只做「激活码」的生成、分发对账与兑换。 3. **节点可秒级灰度**:节点列表带版本,被封节点能即时下架;客户端缓存最后一份可用列表兜底。 4. **最小数据原则(无日志承诺)**:不记录浏览内容/目的地;只保留计费与风控所需的最小元数据(用量字节数、设备数、最后活跃时间),并在文档中公开口径。 @@ -16,19 +16,19 @@ ## 1. 系统拓扑 ``` -┌──────────┐ HTTPS ┌─────────────────┐ -│ Flutter │ ───────▶ │ API (控制面) │── Postgres / Redis -│ 客户端 │ │ Go + chi/gin │ -└────┬─────┘ └───────┬─────────┘ - │ WireGuard UDP │ 节点注册/心跳 (mTLS gRPC) -┌────▼─────────────┐ ┌───────▼─────────┐ -│ 加速节点 (数据面) │ │ 发卡店 Webhook │ -│ wireguard + agent │ │ (激活码入库) │ -└──────────────────┘ └─────────────────┘ +┌────────────────┐ HTTPS ┌─────────────────┐ +│ Flutter 客户端 │ ───────▶ │ API (控制面) │── Postgres / Redis +│ (libbox 内嵌) │ │ Go + chi/gin │ +└───────┬────────┘ └───────┬─────────┘ + │ REALITY / Hy2 │ 节点注册/心跳 (mTLS gRPC) + │ (libbox outbound) ┌────────▼─────────┐ ┌────────────────┐ + └──────────────────▶│ 加速节点 (数据面) │ │ 发卡店 Webhook │ + │ singbox + agent │ │ (激活码入库) │ + └──────────────────┘ └────────────────┘ ``` -- **API**:Go(推荐,wgctrl/生态成熟)。单体起步,按模块分包,未来可拆。 -- **节点 agent**:每台加速节点跑一个轻量 agent:注册、心跳、上报负载、下发/回收 WireGuard peer。与 API 间用 mTLS gRPC。 +- **API**:Go(推荐,chi/gin 生态成熟)。单体起步,按模块分包,未来可拆。 +- **节点 agent**:每台加速节点跑一个轻量 agent:注册、心跳、上报负载、向控制面同步节点凭证(REALITY 公钥/Hy2 口令)。与 API 间用 mTLS gRPC。 - **存储**:Postgres(主数据)+ Redis(验证码、限流、节点实时负载)。 --- @@ -37,15 +37,17 @@ ```sql users (id, email UNIQUE, pw_hash, created_at, status) -devices (id, user_id FK, name, platform, pubkey, last_seen, created_at) +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, pubkey, tags[], tier['free'|'pro'], - status['up'|'down'|'draining'], weight) +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) -- 兑换/封禁/节点操作必记 @@ -68,14 +70,14 @@ POST /v1/auth/refresh {refresh_token} GET /v1/me → 账户 + 订阅 + 用量摘要 GET /v1/me/devices → 设备列表 -DELETE /v1/me/devices/:id → 移除设备(回收节点 peer) +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_pubkey} → 下发 WireGuard 配置(peer 注册) +POST /v1/nodes/:id/connect {device_id} → 下发完整 sing-box config JSON(见 §3.1) POST /v1/nodes/:id/disconnect GET /v1/usage ?days=7 → 用量曲线(统计页) @@ -83,6 +85,63 @@ 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-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. 关键流程 @@ -101,10 +160,15 @@ GET /v1/usage ?days=7 → 用量曲线(统计页) - 风控:单用户兑换失败 5 次/小时锁 1 小时。 ### 4.3 连接(数据面) -1. 客户端拉 `/nodes`(带套餐过滤 + 客户端测延迟自选)。 -2. `POST /nodes/:id/connect`:API 校验订阅与设备数(免费版另校验当日 `ad_unlocked_at` 与剩余分钟)→ 通过 gRPC 让节点 agent 添加 peer → 返回 WireGuard 配置(endpoint、server_pubkey、分配的内网 IP、DNS)。免费版 peer TTL = 剩余分钟数,到时 agent 自动回收。 -3. 心跳/续期:peer 默认 TTL 24h,客户端在线自动续;订阅过期或设备被移除 → agent 回收 peer。 -4. 节点 agent 每 30s 上报负载(在线 peer 数、带宽)→ Redis,用于目录排序与"信号条"。 +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` 轮询(或推送)。 @@ -122,20 +186,39 @@ GET /v1/usage ?days=7 → 用量曲线(统计页) --- ## 6. 部署与运维(起步规模) -- API:单区域 2 实例 + LB;Postgres 托管(每日备份);Redis 托管。 -- 节点:按地区购 VPS(HK/JP/SG/US…),Ansible/脚本一键装 agent + WireGuard;出口 IP 池可轮换。 -- 监控:节点心跳缺失告警、兑换失败率、注册转化漏斗。 -- CI:GitHub Actions——lint + 单测 + OpenAPI 校验 + docker 镜像。 +- 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 的实现顺序 -1. `openapi.yaml`(把 §3 展开成正式契约)+ 数据库 migration -2. **auth 模块**(验证码/注册/登录/JWT)+ 测试 -3. **codes 模块**(批量生成/webhook/兑换)+ 审计 +1. `openapi.yaml`(把 §3/§3.1 展开成正式契约)+ 数据库 migration +2. **auth 模块**(验证码/注册/登录/JWT)+ 测试 +3. **codes 模块**(批量生成/webhook/兑换)+ 审计 4. devices + 订阅校验中间件 -5. nodes 目录 + agent gRPC 协议(proto 文件)+ connect/disconnect +5. nodes 目录 + agent gRPC 协议(proto 文件)+ connect/disconnect(响应体按 §3.1 生成 sing-box config JSON) 6. usage 统计 + 管理端最小后台 -7. Flutter 端接 API(替换 UI Kit 演示数据;UI 规范见 design/) +7. Flutter 端接 API(替换 UI Kit 演示数据;UI 规范见 design/;libbox 桥接见 `doc/plans/11-libbox-bridge.md`) -> 每个模块完成的定义:有单测、有 OpenAPI 同步、错误文案双语且符合脱敏口径。 +> 每个模块完成的定义:有单测、有 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` | diff --git a/doc/plans/11-libbox-bridge.md b/doc/plans/11-libbox-bridge.md new file mode 100644 index 0000000..cc217dd --- /dev/null +++ b/doc/plans/11-libbox-bridge.md @@ -0,0 +1,64 @@ +# 任务 #11 libbox 桥接方案 + +> 任务链:11A(本文,数据面定稿) → 11C(Dart 桥接 PoC) → 11D/11E/11F(隧道/DNS/路由验证) → 11H(M6 联调) + +--- + +## 关键技术选型 —— 数据面协议决策记录 + +**日期**:2026-06-13 +**状态**:已定稿(tsk_9uMrd9kUpmVA) +**影响范围**:#5/#6(控制面 connect 接口实现)· 11C(客户端桥接 PoC)· 11H(M6 联调) + +### 背景 + +`design/server/ARCHITECTURE.md` 初稿(v0.1)将数据面定为 **WireGuard**(peer 注册模式, +节点 agent 下发 endpoint/pubkey/内网 IP/DNS)。但实际情况如下: + +- **现网基础设施**(`deploy/`)已运行 sing-box Hysteria2(UDP/443)和 xray + VLESS+REALITY(TCP/11443),两协议均已通过 phase-0 线路验证(晚高峰 YouTube 1080p + 流畅、REALITY 未见主动探测封锁)。 +- **客户端路线图**(`plan/phase-3-自研与规模化.md`)明确以 **sing-box libbox(gomobile)** + 作为 Flutter 端隧道内核,libbox 原生支持 REALITY/Hy2/WireGuard 三种出站,无需额外 + 桥接层。 +- WireGuard 在 iOS/macOS 沙盒环境下依赖 Network Extension,而 libbox TUN 模式同样需要 + Network Extension,且 libbox 已统一抽象平台差异。 + +### 备选方案对比 + +| 方案 | 优点 | 缺点 | +|------|------|------| +| **A. WireGuard peer 注册(原方案)** | 内核级性能;行业标准 | 需独立 wintun(Win)/ NEPacketTunnel(iOS);与现网基础设施不兼容;节点 agent 需实现 peer CRUD | +| **B. 订阅链接下发(Clash/sing-box URL)** | 服务端极简 | 客户端需内置订阅解析器或依赖三方 App;改品牌客户端难精控格式 | +| **C. libbox + 完整 config JSON 下发**(本方案) | 复用现网 REALITY/Hy2 基础设施;libbox 三协议皆支持;客户端零组装;gomobile 跨 iOS/Android/Desktop;WireGuard 可作 outbound 随时补加 | 服务端需按订阅/设备生成完整 JSON;节点 agent 由 peer 注册改为凭证上报 | + +### 结论 + +采用 **方案 C:libbox 统一承载 REALITY/Hy2 outbound**(必要时追加 WireGuard outbound)。 + +核心理由:**最不后悔原则** —— libbox 三协议皆支持,现网基础设施零改动,客户端接入 +最简,未来切换协议只需修改服务端生成的 JSON,架构不动。 + +### 影响面 + +| 组件 | 变更 | +|------|------| +| `ARCHITECTURE.md §0` | 数据面描述:WireGuard → sing-box libbox (REALITY/Hy2) | +| `ARCHITECTURE.md §1` | 拓扑图:wireguard+agent → singbox+agent | +| `ARCHITECTURE.md §3` | connect 响应:WireGuard 配置 → 完整 sing-box config JSON | +| `ARCHITECTURE.md §4.3` | 连接流程:peer 注册 → config JSON 下发 | +| 节点 agent 职责 | 由 WireGuard peer CRUD → 向控制面注册节点凭证(REALITY 公钥/Hy2 口令)| +| 客户端(11C) | 接收 JSON 后原样传入 `libbox.start(configJson)`,不做任何组装 | +| #5/#6 | connect 接口响应体按 §3.1 规范实现 | +| 11H M6 联调 | 以本契约为准,不依赖旧 WireGuard 格式 | + +--- + +## §3.1 详细契约(摘要见 ARCHITECTURE.md §3) + +完整规范见 `design/server/ARCHITECTURE.md §3` 及 §3.1 子节。 + +示例配置文件:`doc/plans/client-connect-config.example.json` + +> PoC 阶段(11D/11E/11F)可本地直接使用示例文件,不依赖控制面;11H 联调时切换到 +> 控制面动态下发。 diff --git a/doc/plans/client-connect-config.example.json b/doc/plans/client-connect-config.example.json new file mode 100644 index 0000000..2a11e59 --- /dev/null +++ b/doc/plans/client-connect-config.example.json @@ -0,0 +1,99 @@ +{ + "log": { + "level": "warn", + "timestamp": true + }, + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "address": ["172.19.0.1/30"], + "mtu": 9000, + "auto_route": true, + "strict_route": true, + "stack": "system" + } + ], + "outbounds": [ + { + "type": "vless", + "tag": "reality-out", + "server": "__NODE_HOST__", + "server_port": 11443, + "uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "flow": "xtls-rprx-vision", + "tls": { + "enabled": true, + "server_name": "www.apple.com", + "utls": { + "enabled": true, + "fingerprint": "chrome" + }, + "reality": { + "enabled": true, + "public_key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "short_id": "deadbeef" + } + } + }, + { + "type": "hysteria2", + "tag": "hy2-out", + "server": "__NODE_HOST__", + "server_port": 443, + "password": "__HY2_PASSWORD__", + "tls": { + "enabled": true, + "insecure": true, + "alpn": ["h3"] + } + }, + { + "type": "urltest", + "tag": "auto", + "outbounds": ["reality-out", "hy2-out"], + "url": "https://www.gstatic.com/generate_204", + "interval": "3m", + "tolerance": 50 + }, + { + "type": "block", + "tag": "block" + }, + { + "type": "direct", + "tag": "direct" + } + ], + "route": { + "rules": [ + { + "ip_cidr": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "127.0.0.0/8" + ], + "outbound": "direct" + } + ], + "final": "auto", + "auto_detect_interface": true + }, + "dns": { + "servers": [ + { + "tag": "remote", + "address": "tls://8.8.8.8", + "detour": "auto" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "final": "remote", + "strategy": "ipv4_only" + } +}