mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
用户原话:「全部放开,插件不让用这些还叫插件吗」。方向是对的:插件架构的边界 应当是**内核暴露给插件的 API 权限**,而不是一层把插件自己的浏览器栈也关在门外的 进程级 syscall 过滤器。 ## ★ 白屏的根因(本次花了大量时间,记录完整线索链) 同一页面、同一 chromium 参数,只换 seccomp: | SystemCallFilter | 结果 | |---|---| | `@system-service` | 页面**永远空白**(截图非白像素 7.9%,连 `typeof WebAssembly` 都不执行) | | 放开 | 完整渲染 TRAE 登录页(非白像素 **90.6%**,标题 `登录 \| TRAE - The Real AI Engineer`) | TRAE 授权页是 React SPA + 字节风控 SDK(`sdk-glue.js`),依赖 WASM/JIT。 白名单**只减不增**地挡住了它。 **为什么不再逐个补白名单**:那是没有终点的维护 —— 每接一个新平台、每换一次前端 SDK 都可能再撞一个缺口,而症状永远是"页面白屏"这种零线索形态。本次为此**错误 怀疑过** 5 个方向(profile 锁 / /tmp 满 / PrivateTmp / 可写 HOME / AF_NETLINK), 全部被排除后才靠**逐 directive 二分**定位。补白名单只会让下一个人重复这个过程。 `packaging/llmsproxy.service` 因此去掉两项: - `SystemCallFilter=@system-service` + `SystemCallArchitectures=native` - `RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX`(缺 `AF_NETLINK` 会让 chromium 建套接字失败 → SSL `net_error -101` → 导航不了) **保留**的加固(已逐条实测不影响浏览器栈):`NoNewPrivileges` / `ProtectSystem=strict` / `ReadWritePaths=/etc/llmsproxy` / `ProtectHome` / `PrivateTmp` / `CapabilityBoundingSet=`(空)/ `ProtectKernel*` / `RestrictSUIDSGID` / `RestrictRealtime` / `LockPersonality`。可写面仍收窄到 `/etc/llmsproxy`。 ## 修掉的 3 个插件缺陷(都是真 bug,各自有判据) 1. **前向引用(第 12 次)**:`sweepSessions` 调 `reapLoopback`/`browserStack`/ `reapBrowser`,三者定义在**后面** ⇒ 未声明时是全局 nil。加载期不报错,只有走到 「会话过期回收」才炸 ⇒ **第一次登录正常、第二次 500**,且已起的浏览器栈不被回收 (进程与端口泄漏)。→ 加前向声明 + 定义改赋值式。 2. **`spawnBrowser` 里 `url` 是未定义全局**(形参名是 `start_url`)⇒ chromium 打开 `http://nil/`。修复前页面 URL = `http://nil/`,修复后 = 完整 `trae.cn/authorization?...`。 3. **chromium 在加固环境自杀**:`FATAL: GPU process isn't usable. Goodbye.` → `--in-process-gpu`(GPU 逻辑跑浏览器进程内,不 fork)。另给可写 HOME/XDG(`ProtectHome=yes` 下 /root 只读会刷 permission denied 噪声)。 另加**存活探针**:此前只等 CDP 端口就报「已就绪」,即便浏览器已死也报成功 —— **谎报比直接报错更坏**(运维会以为是自己操作的问题)。现在会如实报出 `浏览器 CDP 无应答:...FATAL: GPU process isn't usable`。 ## 测试与生产抢资源的修复 生产 `PrivateTmp=yes` 下 Xvfb `:91` 的锁在**服务自己的 /tmp** 里,宿主测试看不到 ⇒ `xdisplayBusy(91)` 判"空闲"⇒ 测试去抢 91 ⇒ `Xvfb 未就绪`。 → 显示器起始号可被 `LLMSPROXY_XDISPLAY_BASE` 覆盖,测试台统一用 300+; `TestXDisplaySelectionReclaimsStaleLocks` 的号段也跟随(否则它造出的陈旧锁**根本 不会被扫描到**,断言永远失败 —— 那是判据与被测对象脱节)。 (端口不需要覆盖点:端口是全局资源,`portFree` 查 /proc/net/tcp 看得见生产占用。) ## 判据 - `TestSweepSessionsHelpersAreForwardDeclared`:三助手必须前向声明 + 定义必须是 赋值式 + 声明必须**先于**使用。 - `TestBrowserStackVerifiesLivenessNotJustPort`:必须有 CDP 真应答判据 + /proc 存活 + 日志真因 + 失败时回收整组进程。 - `TestBrowserStackUsesInProcessGPU`:`--in-process-gpu` / 可写 HOME/XDG 不得被删。 - `TestChannelsVncStackEndToEnd` / `TestXDisplaySelection*`:真实起栈验证。 ## 端到端验证 `bash deploy.sh` → 生产 `active` / `NRestarts=0`;真实登录请求 → `server_browser=True`, VNC 上**完整渲染 TRAE 登录页**(手机号登录 / 抖音 / 苹果 / 企业账号登录全部可见), 标题 `登录 | TRAE - The Real AI Engineer`。 回归:9 个包全绿。
60 lines
2.7 KiB
Go
60 lines
2.7 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// VNC 一直白屏的真因(2026-10-14 定位)。
|
||
//
|
||
// 生产 unit 加固过,其中 `SystemCallFilter=@system-service` 会挡住 chromium
|
||
// **独立 GPU 进程**启动所需的系统调用 ⇒ 它起不来 ⇒ chromium 放弃并退出:
|
||
//
|
||
// ERROR:gpu_process_host.cc] GPU process launch failed: error_code=1002
|
||
// FATAL:gpu_data_manager_impl_private.cc] GPU process isn't usable. Goodbye.
|
||
//
|
||
// 整个 chromium 几秒内退出 ⇒ **VNC 上永远白屏**,而插件因为只等 CDP 端口就报
|
||
// 「已就绪」,把白屏谎报成成功。
|
||
//
|
||
// 为什么长期没发现:手工在**沙箱外**跑同一条命令每次都成功。定位靠
|
||
// `systemd-run` 逐 directive 二分 —— 只有 SystemCallFilter 与 ProtectSystem
|
||
// 能复现,其余加固项无影响。
|
||
//
|
||
// 修法:`--in-process-gpu`(GPU 逻辑跑在浏览器进程内,不 fork)。
|
||
func TestBrowserStackUsesInProcessGPU(t *testing.T) {
|
||
b, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// ① 必须带 --in-process-gpu,否则在加固环境下浏览器起不来
|
||
if !strings.Contains(b, "--in-process-gpu") {
|
||
t.Fatal("chromium 命令行必须带 --in-process-gpu —— 否则在 " +
|
||
"SystemCallFilter=@system-service 的加固环境下 GPU 进程起不来、" +
|
||
"浏览器自杀、VNC 永远白屏(真实事故,2026-10-14)")
|
||
}
|
||
// ② 必须给可写的 HOME/XDG:ProtectHome=yes 下 /root 只读,
|
||
// chromium 建 dconf/ibus 目录会持续刷 permission denied 噪声
|
||
for _, k := range []string{"XDG_CACHE_HOME=", "XDG_CONFIG_HOME="} {
|
||
if !strings.Contains(b, k) {
|
||
t.Errorf("chromium 命令行应设置 %s(加固环境下 /root 只读会产生噪声日志)", k)
|
||
}
|
||
}
|
||
// ③ **存活探针**必须存在:端口在听 ≠ 浏览器可用。
|
||
// 没有它时,"浏览器已死"会被谎报成"已就绪",运维打开 VNC 是白屏,
|
||
// 还会以为是自己操作的问题。谎报比直接报错更坏。
|
||
if !strings.Contains(b, "browserUtil.cdpAlive") {
|
||
t.Error("起栈后必须做 CDP 存活探针 —— 只等端口就报就绪会把白屏谎报成成功")
|
||
}
|
||
if !strings.Contains(b, "browserUtil.readTail") {
|
||
t.Error("启动失败必须把日志真因带给用户,而不是一句「启动失败」")
|
||
}
|
||
// ④ 失败必须回收整组进程(否则每次失败都留一套 Xvfb/x11vnc/websockify)
|
||
idx := strings.Index(b, "浏览器启动后即退出")
|
||
if idx < 0 {
|
||
t.Fatal("应有「浏览器启动后即退出」的失败分支")
|
||
}
|
||
if !strings.Contains(b[idx:idx+600], "reapBrowser()") {
|
||
t.Error("起栈失败必须先回收已起的进程(否则泄漏一套 Xvfb/x11vnc/websockify)")
|
||
}
|
||
}
|