docs(agent): 记录 auth_user 血泪教训 + 客户端隧道前置 + 验收须真连接

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-24 07:39:39 +08:00
parent 2abe4d0816
commit 344dfe9a82
3 changed files with 22 additions and 7 deletions
+11 -4
View File
@@ -114,10 +114,11 @@
<pre><code>"route": {
"rules": [
{"action": "sniff"},
{"action": "resolve"},
{"user": ["&lt;我的uuid…&gt;"], "domain": ["brain.51yanmei.com","git.51yanmei.com"],
{"auth_user": ["&lt;我的uuid…&gt;"], "domain": ["brain.51yanmei.com","git.51yanmei.com"],
"outbound": "direct"},
{"user": ["&lt;我的uuid…&gt;"], "ip_cidr": ["182.92.213.171/32"],
{"auth_user": ["&lt;我的uuid…&gt;"], "ip_cidr": ["182.92.213.171/32"],
"port": [5001,3389,10022,10023], "outbound": "direct"},
{"domain": ["brain.51yanmei.com","git.51yanmei.com"], "action": "reject"},
@@ -128,7 +129,11 @@
],
"final": "direct"
}</code></pre>
<p class="small">已用本机 sing-box 1.13.13 <code>sing-box check</code> 验证形状合法(exit 0)。</p>
<div class="card bad">
<h3>血泪教训:VLESS 只认 <code>auth_user</code>,不认 <code>user</code>2026-07-23 生产验证)</h3>
<p>放行规则匹配凭证必须用 <b><code>auth_user</code></b> 而非 <code>user</code><code>sing-box check</code> 对两者<b>都语法通过</b>,但 <code>user</code> 字段对 VLESS/REALITY 入站<b>运行时根本不匹配</b>——放行规则永不命中,结果<b>连白名单用户也被兜底拒绝</b>(全员进不去)。上线时正是踩了这个:节点端 git 一直 000,把本机 uuid 换着法加进白名单都没用。本地起一对真 VLESS 实例实测才定位:<code>auth_user:["good"]</code> 生效(good 通/bad 被 block),<code>user</code> 不生效。<b>只跑 <code>sing-box check</code> 不足以验收访问控制,必须真连接跑一次。</b></p>
</div>
<p class="small">已用本机 sing-box 1.13.13 <b>真 VLESS 连接</b>验证 <code>auth_user</code> 匹配 + <code>resolve</code> 后 ip_cidr/domain 仍匹配;节点端以 git 做「白名单一进一出」验证 per-user 放行/拒绝生效。</p>
<div class="card root">
<h3>规则顺序的三条硬约束</h3>
@@ -206,7 +211,8 @@ journalctl -u pangolin-agent -n 20 # 确认已重渲染、无 ACL ERROR</c
<li>渲染:四态矩阵(ACL×WARP 开关)逐一断言 route 结构</li>
<li>顺序:<code>sniff</code> 唯一且最先;放行先于拒绝;ACL 全部先于 WARP</li>
<li>对称性:同一 target 的放行与拒绝规则,目的地条件逐字相同</li>
<li>产物合法性:渲染结果喂 <code>sing-box check</code> 通过</li>
<li>产物合法性:渲染结果喂 <code>sing-box check</code> 通过<b>注意:check 只验语法,不验 <code>auth_user</code> 运行时是否真匹配 VLESS——见 §4.2 血泪教训</b></li>
<li><b>运行时匹配(不可省)</b>:起真 VLESS 连接实测 <code>auth_user</code> 放行/拒绝生效;或节点端以某个白名单域名做「本机 uuid 一进一出」验证 per-user 生效。仅 <code>sing-box check</code> 绿=未验收。</li>
</ul>
<h3>上线验收</h3>
<ol>
@@ -235,6 +241,7 @@ journalctl -u pangolin-agent -n 20 # 确认已重渲染、无 ACL ERROR</c
<li><span class="tag warn">泄露</span> 节点配置里我的几台设备 uuid 会出现在同一条规则中(自我关联),且目的地明文可见。目的地本就在公网 DNS 里,泄露面小;但这确实<b>弱化了 <code>render.go:14</code> 的「节点只见不透明 dp_uuid」不变式</b>,属于知情接受,需在该注释处补一行说明。</li>
<li><span class="tag warn">缺闸</span> 守红线的 <code>assertNoIdentityFields</code><code>agentd/singbox_test.go</code>)只作用于 <code>state.json</code><b>从不检查渲染出的 sing-box 配置</b>。本设计不扩大这个缺口,但也没有补上——补闸另开。</li>
<li><span class="tag bad">架构限制</span> <b>443 vhost 的判定依赖 SNI,可被绕过。</b><code>brain</code>/<code>git</code>,节点侧唯一的区分手段是 sniff 出的 SNI。攻击者(持有效 dp_uuid 的其他 pangolin 用户)向 <code>182.92.213.171:443</code> 发起<b>不带 SNI</b> 的 TLS、握手后用 <code>Host: brain.51yanmei.com</code> 头访问,则不匹配任何 <code>domain</code> 规则、也不匹配 <code>ip_cidr</code>443 不在端口清单里)→ 落 <code>final:direct</code> → nginx 按 Host 路由放行。这在 sing-box 层<b>无法闭合</b>——不能整封 <code>:443</code>,否则 jiu/travel/sudoku/pay 一起死。因此下面这条 brain 鉴权不是「可选纵深」,而是本闸对 brain 的<b>前置条件</b></li>
<li><span class="tag warn">客户端前置</span> <b>私有域名必须走隧道,否则本闸无从谈起。</b>节点 ACL 只对经隧道进入 sing-box 的流量生效;若客户端把 <code>brain</code>/<code>nas</code> 直连(smartRoute 把国内 IP 分流成直连),则流量根本不到节点、直接打 ali,brain 得 403(nginx deny 非白名单源 IP)、nas 超时。故 <code>brain/nas/git/win.51yanmei.com</code> 必须在控制面 <code>PANGOLIN_PRIVATE_SPLIT_DOMAINS</code> 里(已配),且<b>客户端改动后要重连一次</b>才拿到新分流规则。上线验证时 git 走隧道正常、brain/nas 因客户端未重连仍直连——排查时先确认「域名是否真走了隧道」(curl -v 看连的是不是 pangolin 出口),再判 ACL。</li>
<li><span class="tag ok">已做(2026-07-23</span> <b>brain 已加 nginx basic auth</b> 作纵深防御:ali 的 <code>/etc/nginx/conf.d/brain.conf</code> 在原有 <code>allow 103.119.13.48; deny all</code> 之上叠加 <code>auth_basic</code><code>satisfy</code> 默认 <code>all</code> → 源 IP 白名单 <b></b> 口令二者都需满足),口令存 Bitwarden「brain basic auth」,htpasswd 仅存 apr1 哈希(明文不落 ali)。acme 通道(:80)与 <code>robots.txt</code> 免密。这样即便本节点 ACL 闸失效(agent 挂了、配置手抖)或被上面的 SNI 手法绕过,brain——唯一无自带鉴权的私有服务——仍不裸奔。</li>
</ul>
+9 -3
View File
@@ -59,6 +59,7 @@
<li>每个 Task 结束必须 <code>cd server &amp;&amp; go build ./... &amp;&amp; go test ./internal/agentd/...</code> 全绿再提交。</li>
<li>Commit message 前缀用 <code>feat(agent):</code> / <code>test(agent):</code> / <code>refactor(agent):</code></li>
</ul>
<blockquote><p><b>执行后修订(2026-07-23,提交 <code>2abe4d0</code>)· 血泪教训</b>:放行规则匹配凭证必须用 <b><code>auth_user</code></b> 而非本计划各处写的 <code>user</code><code>sing-box check</code> 对两者都语法通过,但 <code>user</code> 字段对 VLESS/REALITY 入站<b>运行时不匹配</b>,导致放行规则永不命中、<b>连白名单用户也被兜底拒绝</b>(全员进不去)。生产上线时踩中,本地起真 VLESS 连接实测才定位。<b>凡本计划(Task 2 <code>rules()</code>、Task 3 测试)出现 <code>r[&quot;user&quot;] = uuids</code> 或断言 <code>r[&quot;user&quot;]</code> 之处,一律应为 <code>auth_user</code></b> 且验收不能只跑 <code>sing-box check</code>——必须真连接跑一次(节点端「白名单 uuid 一进一出」即可)。</p></blockquote>
<hr>
<h3>Task 1: ACL 配置类型、加载与 fail-closed 的 active()</h3>
<p><b>Files:</b></p>
@@ -1068,14 +1069,19 @@ git commit -m &quot;docs(agent): 私有目的地 ACL 配置样例与常驻校验
<ol>
<li><code>/etc/pangolin-agent/acl.json</code>(权限 <code>0600</code>,属主 <code>pangolin</code>),<code>systemctl reload pangolin-agent</code></li>
<li><code>journalctl -u pangolin-agent -n 30</code> 确认已重渲染、无 <code>[acl] ERROR</code> / <code>[acl] ALERT</code></li>
<li><code>sudo python3 -c &quot;import json;print(json.load(open(&#x27;/etc/sing-box/config.json&#x27;))[&#x27;route&#x27;])&quot;</code> 确认规则顺序为 sniff → 放行 → 拒绝 → warp。</li>
<li><code>sudo python3 -c &quot;import json;print(json.load(open(&#x27;/etc/sing-box/config.json&#x27;))[&#x27;route&#x27;])&quot;</code> 确认规则顺序为 sniff → resolve → 放行 → 拒绝 → warpresolve 见 I3:堵住 ip_cidr 目的地的域名形式绕过)</li>
</ol>
<p> <b><code>config.json</code> 内容正常不等于线上生效</b>——SIGHUP 走的是 sing-box 自身校验,若新配置被 sing-box 拒绝,它会保留旧实例继续跑,agent 侧仍记&quot;渲染成功&quot;。reload 后必须额外确认:</p>
<ul>
<li><code>systemctl is-active sing-box</code><code>active</code></li>
<li><code>journalctl -u sing-box -n5 | grep -v FATAL</code>(有 FATAL 说明 sing-box 拒绝了新配置,旧实例还在跑,config.json 上的内容其实没生效)。</li>
<li><b>我的设备</b>brain 首页 200、DSM 5001 可登录、<code>ssh nas-r</code> 通。</li>
<li><b>另一账号的设备</b>:以上全部被拒(连接被 reject,不是超时)。</li>
<li><b>另一账号的设备</b>:以上全部被拒(连接被 reject,不是超时)。<b>分别用域名和裸 IP 两种形式各测一遍</b>(如 <code>curl https://brain.51yanmei.com</code><code>curl --resolve brain.51yanmei.com:443:103.119.13.48 https://brain.51yanmei.com</code>DSM 同理分别用 <code>nas.51yanmei.com:5001</code><code>182.92.213.171:5001</code>)——I3 表明目的地表达形式(域名 vs IP)会走到不同的 sing-box 匹配路径,只测一种形式验证不到位。</li>
<li><b>公开站不受影响</b>jiu / travel / sudoku / pay 在两个账号下均正常。</li>
<li><b>WARP 未被破坏</b>reddit 仍走 warp 出口。</li>
<li><b>fail-closed 实证</b>:把 <code>acl.json</code> 改坏 → <code>systemctl reload pangolin-agent</code> → 规则仍在、日志有 ERROR;恢复文件。</li>
<li><b>不断线实证</b>:reload 期间另一台设备保持连接不掉。</li>
</ol>
</ul>
<h2>不在本计划范围内</h2>
<ul>
<li>给 brain 加 mTLS / basic auth 作纵深防御(与本计划正交,约一小时,另开)</li>
@@ -19,6 +19,8 @@
- 每个 Task 结束必须 `cd server && go build ./... && go test ./internal/agentd/...` 全绿再提交。
- Commit message 前缀用 `feat(agent):` / `test(agent):` / `refactor(agent):`
> **执行后修订(2026-07-23,提交 `2abe4d0`)· 血泪教训**:放行规则匹配凭证必须用 **`auth_user`** 而非本计划各处写的 `user`。`sing-box check` 对两者都语法通过,但 `user` 字段对 VLESS/REALITY 入站**运行时不匹配**,导致放行规则永不命中、**连白名单用户也被兜底拒绝**(全员进不去)。生产上线时踩中,本地起真 VLESS 连接实测才定位。**凡本计划(Task 2 `rules()`、Task 3 测试)出现 `r["user"] = uuids` 或断言 `r["user"]` 之处,一律应为 `auth_user`。** 且验收不能只跑 `sing-box check`——必须真连接跑一次(节点端「白名单 uuid 一进一出」即可)。
---
### Task 1: ACL 配置类型、加载与 fail-closed 的 active()