Files
pangolin/docs/configurable-proxy-spec.html
T

183 lines
15 KiB
HTML
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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pangolin 可配置分流 · 配置说明</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#262b36; --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
--mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
}
@media (prefers-color-scheme: light){:root{--bg:#f4f5f8;--panel:#fff;--panel2:#eef0f4;--fg:#1a1d24;--fg2:#5a6172;--border:#dde0e7}}
:root[data-theme="dark"]{--bg:#0f1117;--panel:#171a22;--panel2:#1d2129;--fg:#e6e8ee;--fg2:#a8afbd;--border:#262b36}
:root[data-theme="light"]{--bg:#f4f5f8;--panel:#fff;--panel2:#eef0f4;--fg:#1a1d24;--fg2:#5a6172;--border:#dde0e7}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:860px;margin:0 auto;padding:34px 22px 90px}
h1{font-size:25px;margin:0 0 4px}
.lead{color:var(--fg2);margin:0 0 8px;font-size:14.5px}
h2{font-size:18px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:6px;margin:40px 0 14px}
h3{font-size:15.5px;color:var(--accent2);margin:24px 0 8px}
p{margin:9px 0}
code{font-family:var(--mono);font-size:.87em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:#cdd3df}
table{border-collapse:collapse;width:100%;font-size:13.5px;margin:12px 0;display:block;overflow-x:auto}
th,td{border:1px solid var(--border);padding:8px 11px;text-align:left;vertical-align:top}
th{background:var(--panel2);color:var(--accent2);font-weight:600;white-space:nowrap}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:4px 18px;margin:14px 0}
.tag{display:inline-block;font-size:11px;padding:1px 8px;border-radius:20px;font-family:var(--mono)}
.t-direct{background:rgba(94,194,122,.16);color:var(--ok)} .t-proxy{background:rgba(224,136,79,.18);color:var(--accent)} .t-reject{background:rgba(224,106,106,.16);color:var(--bad)}
.box{border-left:3px solid var(--accent);background:var(--panel);border-radius:0 10px 10px 0;padding:12px 16px;margin:14px 0;font-size:14px}
.box.warn{border-color:var(--warn)} .box.q{border-color:var(--accent2)}
ul{padding-left:22px} li{margin:5px 0}
.step{display:flex;gap:10px;align-items:baseline;margin:6px 0}
.step b{color:var(--accent2);font-family:var(--mono);font-size:13px}
a{color:var(--accent2)}
</style>
</head>
<body>
<div class="wrap">
<h1>Pangolin 可配置分流 · 配置说明文档</h1>
<p class="lead">Task 3 设计 · 让用户像 Shadowrocket 那样自定义路由规则(哪些直连 / 走隧道 / 拒绝)。</p>
<p class="lead">配套视觉原型:<a href="configurable-proxy-prototype.html">交互原型 →</a> · 状态:<b style="color:var(--ok)">Phase 1 已实现</b>(feat/configurable-proxy;实现计划 <a href="configurable-proxy-plan.html">阅读版</a>)</p>
<h2>1. 目标与背景</h2>
<p>当前 pangolin 客户端的分流是<b>服务端固定渲染</b>的:LAN 直连、DNS 劫持、可选国内分流(geoip/geosite-cn)、私有域名走隧道,用户无法自定义。实际需求(如 CI 编译源、公司内网、某些国内站要直连;某些站要强制走隧道;广告要拒绝)无处配置——这次暴露的「CI 流量被灌进隧道拖垮小节点」就是典型。</p>
<p>本设计给用户一套<b>可配置路由规则</b>,心智模型对齐 Shadowrocket 的 <code>[Rule]</code> 段:<b>有序规则表,首命中生效</b>,动作三选一(直连 / 走隧道 / 拒绝)。</p>
<h2>2. 架构铁律:客户端不拼配置</h2>
<div class="box warn"><code>ARCHITECTURE.md §3.1</code>:Dart/Flutter 客户端<b>不得</b>自行拼装或修改 sing-box 配置——配置一律服务端渲染、原样下发。所以"客户端可配置"不能变成"客户端本地改 config"。</div>
<p>解法:<b>用户在 App 编规则 → 存为服务端 per-user「路由档案(routing profile)」→ connect 时服务端把档案翻译进渲染的配置</b>。客户端只负责编辑 UI + 存/取档案,永不碰 sing-box JSON。</p>
<pre>App 规则编辑器 ──PUT /v1/routing-profile──▶ 服务端存 per-user 档案(DB)
App 点连接 ──POST /v1/nodes/{id}/connect──▶ BuildClientConfig(读档案→翻译成 route.rules)
App ◀────────── 完整 sing-box 配置(含用户规则)─────┘ 原样喂内核</pre>
<h2>3. 规则模型</h2>
<p>一条规则 = <code>{ type, value, action, note?, enabled }</code>。整个档案:</p>
<pre>{
"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" // 兜底:未命中任何规则的动作
}</pre>
<h3>类型 type</h3>
<table>
<tr><th>type</th><th>值示例</th><th>sing-box 字段</th></tr>
<tr><td><code>domain</code></td><td>example.com</td><td><code>domain</code>(精确)</td></tr>
<tr><td><code>domain_suffix</code></td><td>aliyun.com</td><td><code>domain_suffix</code>(含子域)</td></tr>
<tr><td><code>domain_keyword</code></td><td>google</td><td><code>domain_keyword</code></td></tr>
<tr><td><code>ip_cidr</code></td><td>35.190.0.0/16 · ::/0</td><td><code>ip_cidr</code>(v4/v6)</td></tr>
<tr><td><code>geoip</code></td><td>CN · US</td><td><code>rule_set</code>(自托管 geoip-*.srs)</td></tr>
<tr><td><code>geosite</code></td><td>cn · netflix · category-ads</td><td><code>rule_set</code>(自托管 geosite-*.srs)</td></tr>
</table>
<h3>动作 action → sing-box outbound</h3>
<table>
<tr><th>action</th><th>outbound</th><th>说明</th></tr>
<tr><td><span class="tag t-direct">direct</span></td><td><code>direct</code></td><td>物理网卡直连(配合 route_exclude/reverse_mapping 真直连)</td></tr>
<tr><td><span class="tag t-proxy">proxy</span></td><td><code>auto</code></td><td>经节点(REALITY/Hy2 urltest 择优)</td></tr>
<tr><td><span class="tag t-reject">reject</span></td><td><code>block</code></td><td>阻断</td></tr>
</table>
<h2>4. 优先级(渲染顺序)</h2>
<p>服务端把规则按固定层级拼进 <code>route.rules</code>,<b>自上而下首命中</b>:</p>
<div class="card">
<div class="step"><b>1</b><span>DNS 劫持(port 53 → hijack-dns)· <b>系统强制</b></span></div>
<div class="step"><b>2</b><span>LAN / 私网直连(10/8·192.168/16·127/8)· <b>系统强制</b></span></div>
<div class="step"><b>3</b><span>私有服务域名 → 走隧道(<code>PANGOLIN_PRIVATE_SPLIT_DOMAINS</code><b>服务端</b></span></div>
<div class="step"><b>4</b><span><b>用户规则</b>(档案 <code>rules[]</code> 顺序展开)← 新增</span></div>
<div class="step"><b>5</b><span>国内分流 geoip-cn/geosite-cn → 直连(<code>china_direct</code> 开时)</span></div>
<div class="step"><b>6</b><span>FINAL 兜底(<code>final</code>:proxy / direct)</span></div>
</div>
<div class="box">用户规则排在国内分流<b>之前</b>——这样"我要 github 走隧道""我要某国内站走隧道"能压过 geoip-cn 的直连默认。系统层(1-3)永远在用户规则之上,防止用户误配把 DNS/LAN/私有服务弄坏。</div>
<h3>默认规则 vs 自定义规则 · 冲突处理</h3>
<p>确定性,不靠猜——冲突由<b>层级 + 顺序</b>唯一裁决:</p>
<ul>
<li><b>系统强制层永远赢</b>(层1-3:DNS劫持/LAN/私有服务走隧道):在用户规则之上,用户不可覆盖。针对这些目标不存在"冲突"。</li>
<li><b>用户规则赢过智能分流</b>(层4 在层5 前):用户显式规则压过 geoip-cn 的宽泛默认——这是用户意图应有的优先级。</li>
<li><b>用户规则内部:有序,首命中赢</b>(同 Shadowrocket)。UI 支持拖动排序。</li>
</ul>
<p><b>UI 主动提示两类冲突</b>(编辑器里实时标记,不静默):</p>
<table>
<tr><th>冲突</th><th>提示</th></tr>
<tr><td>被前面规则遮蔽、永不命中</td><td>规则灰掉 + 「已被上面「X」覆盖,永不命中」</td></tr>
<tr><td>撞到锁定的系统目标(如给私有服务域名加规则)</td><td>「该域名由系统强制走隧道,此规则不生效」</td></tr>
</table>
<h3>三模式语义 · 用户规则仅「智能分流」生效</h3>
<table>
<tr><th>模式</th><th>行为</th><th>用户规则</th></tr>
<tr><td>智能分流(默认)</td><td>完整规则链:系统层 → 用户规则 → 国内分流 → FINAL</td><td><b>生效</b></td></tr>
<tr><td>全局代理</td><td>除系统层(LAN/私有服务)外全走隧道</td><td>忽略</td></tr>
<tr><td>全部直连</td><td>除系统层(私有服务走隧道)外全直连</td><td>忽略</td></tr>
</table>
<p><b>系统层(层1-3)在任何模式都强制生效</b>——全局/直连只改 FINAL 兜底 + 是否套用用户规则,不会关掉 DNS 劫持/LAN 直连/私有服务。UI 在非智能分流模式下把「我的规则」区灰化并注明"当前模式忽略以下规则"。</p>
<h3>与现有 Settings「Smart routing」的衔接</h3>
<p>现在 Settings 的 <code>Smart routing</code> 是二元开关(= <code>builtin.china_direct</code>)。改为<b>渐进式披露入口</b>:该行变成「分流规则」设置行(右侧显示当前模式 + chevron →),点进去即分流规则屏。休闲用户维持"智能分流"不点即可(等价现在开关开着),高级用户点进去自定义。老的二元开关被三模式(全局/智能分流/直连)涵盖。</p>
<h2>5. 直连是否真"不走 VPN"</h2>
<p><code>direct</code> outbound 让 sing-box 从物理网卡直接出连接。但 TUN <code>strict_route</code> 会把包重新捕回隧道——已有两个机制解决,用户规则复用:</p>
<ul>
<li><b>IP 段直连</b>:靠 <code>route_exclude_address</code>(在 auto_route 层排除,已用于 LAN)。用户加的 <code>ip_cidr → direct</code> 需同步进 route_exclude,否则被 strict_route 抓回。</li>
<li><b>域名直连</b>:靠 <code>dns.reverse_mapping</code>(记 IP↔域名,按 IP 连接时补回域名元数据让 domain 规则命中)+ local DNS(223.5.5.5,国内解析)。已用于私有域名分流,机制现成。</li>
</ul>
<div class="box warn">这是<b>关键实现难点</b>:域名类直连规则要真生效,必须开 reverse_mapping 且用 local DNS 解析该域名;IP 类直连规则要真生效,值必须同时进 route_exclude_address。这两点在实现计划里逐条落。</div>
<h2>6. 存储与传输(API)</h2>
<table>
<tr><th>端点</th><th>作用</th></tr>
<tr><td><code>GET /v1/routing-profile</code></td><td>拉当前用户档案(App 编辑器初始化;无则返回内置默认)</td></tr>
<tr><td><code>PUT /v1/routing-profile</code></td><td>保存档案(服务端校验:CIDR 合法、type 合法、条数上限、去重)</td></tr>
<tr><td><code>POST …/connect</code>(现有)</td><td>渲染时读该用户档案,翻译进 <code>route.rules</code></td></tr>
</table>
<p>档案存 DB(<code>routing_profiles</code> 表:<code>user_id · profile_json · updated_at</code>),不走 connect body(与现有 <code>split_cn</code> 走 query 的约束一致——大规则集不塞 query/body)。客户端本地也缓存一份(离线可看/编,连接时以服务端为准)。</p>
<h2>7. 校验与兜底(fail-safe)</h2>
<ul>
<li>PUT 时服务端校验每条规则:<code>ip_cidr</code> 可解析、<code>type</code> 在白名单、<code>geosite/geoip</code> 值在自托管规则集清单内、总条数 ≤ 上限(如 200)。非法整体拒绝(4xx + 逐条错误),不半保存。</li>
<li>渲染时档案损坏/为空 → 回退内置默认(等于现在的行为),绝不产出坏配置(参考 agent ACL 的 fail-closed/last-good 思路)。</li>
<li>系统层(DNS/LAN/私有服务)永远强制,用户规则无法覆盖或删除。</li>
</ul>
<h2>8. 与现有机制的关系</h2>
<table>
<tr><th>现有</th><th>本设计如何吸收</th></tr>
<tr><td>SplitCN(geoip/geosite-cn 直连,#5)</td><td>降为档案里的 <code>builtin.china_direct</code> 开关(层级 5)</td></tr>
<tr><td>PrivateSplitDomains(走隧道)</td><td>保留为系统层 3(服务端 env,用户不可动)</td></tr>
<tr><td>route_exclude_address(LAN)</td><td>保留 + 扩展:用户 ip_cidr→direct 规则动态并入</td></tr>
<tr><td>reverse_mapping / local DNS</td><td>复用:域名直连规则靠它命中</td></tr>
</table>
<p>换句话说,本设计是把三个散落的分流机制<b>统一收进一个用户可见、可配的规则模型</b>,底层复用已验证的 sing-box 手法。</p>
<h2>9. 待你确认的取舍</h2>
<div class="box q"><b>Q1 规则粒度</b>:先做"域名/IP/GeoIP/GeoSite + 三动作"的规则表(本设计)?还是要更细(URL 正则、UA、进程名)?建议先前者,后者 shadowrocket 也少人用。</div>
<div class="box q"><b>Q2 分应用代理(per-app)</b>:按 App 分流(仅 Android/桌面可行,iOS 系统扩展做不到)。较重,<b>建议后置</b>为独立任务。</div>
<div class="box q"><b>Q3「从文本导入」</b>:粘贴 Shadowrocket/Clash 规则批量建(原型第三屏)。锦上添花,<b>可 Phase 2</b></div>
<div class="box q"><b>Q4 档案作用域</b>:per-user(跨设备同步,推荐)还是 per-device(每台独立)?推荐 per-user + 单档案起步。</div>
<h2>10. 分期建议</h2>
<ul>
<li><b>Phase 1(核心)</b>:routing_profiles 表 + API + BuildClientConfig 翻译(域名/IP/GeoIP/GeoSite × 直连/走隧道/拒绝)+ 移动端规则表 UI(增删改排序)+ 校验/兜底 + reverse_mapping/route_exclude 打通。</li>
<li><b>Phase 2</b>:从文本导入、内置规则模板(常用国内直连包/广告拒绝包一键加)、桌面端 UI。</li>
<li><b>Phase 3(可选)</b>:分应用代理、规则订阅 URL。</li>
</ul>
<p style="margin-top:24px;color:var(--fg2)">确认这份设计后,我用 <code>writing-plans</code> 出 Phase 1 的实现计划(TDD、多端、服务端翻译逐条测),再开发。</p>
</div>
</body>
</html>