Task 3 设计 · 让用户像 Shadowrocket 那样自定义路由规则(哪些直连 / 走隧道 / 拒绝)。
配套视觉原型:交互原型 → · 状态:设计待确认(未开发)
当前 pangolin 客户端的分流是服务端固定渲染的:LAN 直连、DNS 劫持、可选国内分流(geoip/geosite-cn)、私有域名走隧道,用户无法自定义。实际需求(如 CI 编译源、公司内网、某些国内站要直连;某些站要强制走隧道;广告要拒绝)无处配置——这次暴露的「CI 流量被灌进隧道拖垮小节点」就是典型。
本设计给用户一套可配置路由规则,心智模型对齐 Shadowrocket 的 [Rule] 段:有序规则表,首命中生效,动作三选一(直连 / 走隧道 / 拒绝)。
ARCHITECTURE.md §3.1:Dart/Flutter 客户端不得自行拼装或修改 sing-box 配置——配置一律服务端渲染、原样下发。所以"客户端可配置"不能变成"客户端本地改 config"。解法:用户在 App 编规则 → 存为服务端 per-user「路由档案(routing profile)」→ connect 时服务端把档案翻译进渲染的配置。客户端只负责编辑 UI + 存/取档案,永不碰 sing-box JSON。
App 规则编辑器 ──PUT /v1/routing-profile──▶ 服务端存 per-user 档案(DB)
│
App 点连接 ──POST /v1/nodes/{id}/connect──▶ BuildClientConfig(读档案→翻译成 route.rules)
│
App ◀────────── 完整 sing-box 配置(含用户规则)─────┘ 原样喂内核
一条规则 = { type, value, action, note?, enabled }。整个档案:
{
"mode": "rule", // global | rule | direct(对齐 shadowrocket 三模式)
"builtin": {
"china_direct": true, // 国内分流(geoip-cn/geosite-cn → 直连)开关
"lan_direct": true, // LAN/私网直连(强制,不可关)
"private_via_tunnel": true // 私有服务域名走隧道(服务端下发,不可关)
},
"rules": [ // 用户自定义,有序,首命中生效
{ "type": "domain_suffix", "value": "git.51yanmei.com", "action": "direct", "note": "CI 源", "enabled": true },
{ "type": "geosite", "value": "category-ads", "action": "reject" },
{ "type": "ip_cidr", "value": "35.190.0.0/16", "action": "proxy" }
],
"final": "proxy" // 兜底:未命中任何规则的动作
}
| type | 值示例 | sing-box 字段 |
|---|---|---|
domain | example.com | domain(精确) |
domain_suffix | aliyun.com | domain_suffix(含子域) |
domain_keyword | domain_keyword | |
ip_cidr | 35.190.0.0/16 · ::/0 | ip_cidr(v4/v6) |
geoip | CN · US | rule_set(自托管 geoip-*.srs) |
geosite | cn · netflix · category-ads | rule_set(自托管 geosite-*.srs) |
| action | outbound | 说明 |
|---|---|---|
| direct | direct | 物理网卡直连(配合 route_exclude/reverse_mapping 真直连) |
| proxy | auto | 经节点(REALITY/Hy2 urltest 择优) |
| reject | block | 阻断 |
服务端把规则按固定层级拼进 route.rules,自上而下首命中:
PANGOLIN_PRIVATE_SPLIT_DOMAINS)· 服务端rules[] 顺序展开)← 新增china_direct 开时)final:proxy / direct)direct outbound 让 sing-box 从物理网卡直接出连接。但 TUN strict_route 会把包重新捕回隧道——已有两个机制解决,用户规则复用:
route_exclude_address(在 auto_route 层排除,已用于 LAN)。用户加的 ip_cidr → direct 需同步进 route_exclude,否则被 strict_route 抓回。dns.reverse_mapping(记 IP↔域名,按 IP 连接时补回域名元数据让 domain 规则命中)+ local DNS(223.5.5.5,国内解析)。已用于私有域名分流,机制现成。| 端点 | 作用 |
|---|---|
GET /v1/routing-profile | 拉当前用户档案(App 编辑器初始化;无则返回内置默认) |
PUT /v1/routing-profile | 保存档案(服务端校验:CIDR 合法、type 合法、条数上限、去重) |
POST …/connect(现有) | 渲染时读该用户档案,翻译进 route.rules |
档案存 DB(routing_profiles 表:user_id · profile_json · updated_at),不走 connect body(与现有 split_cn 走 query 的约束一致——大规则集不塞 query/body)。客户端本地也缓存一份(离线可看/编,连接时以服务端为准)。
ip_cidr 可解析、type 在白名单、geosite/geoip 值在自托管规则集清单内、总条数 ≤ 上限(如 200)。非法整体拒绝(4xx + 逐条错误),不半保存。| 现有 | 本设计如何吸收 |
|---|---|
| SplitCN(geoip/geosite-cn 直连,#5) | 降为档案里的 builtin.china_direct 开关(层级 5) |
| PrivateSplitDomains(走隧道) | 保留为系统层 3(服务端 env,用户不可动) |
| route_exclude_address(LAN) | 保留 + 扩展:用户 ip_cidr→direct 规则动态并入 |
| reverse_mapping / local DNS | 复用:域名直连规则靠它命中 |
换句话说,本设计是把三个散落的分流机制统一收进一个用户可见、可配的规则模型,底层复用已验证的 sing-box 手法。
确认这份设计后,我用 writing-plans 出 Phase 1 的实现计划(TDD、多端、服务端翻译逐条测),再开发。